diff --git a/.env.ci b/.env.ci index c459c928..f25246e7 100644 --- a/.env.ci +++ b/.env.ci @@ -23,7 +23,8 @@ ATTESTATION_HOST = "attestation" ATTESTATION_PORT = 8080 # nilAuth Trusted URLs -NILAUTH_TRUSTED_ROOT_ISSUERS = "http://nilauth:30921" +NILAUTH_TRUSTED_ROOT_ISSUERS = "http://nilauth-credit-server:3000" # "http://nilauth:30921" +CREDIT_API_TOKEN = "n i l l i o n" # Postgres Docker Compose Config POSTGRES_HOST = "postgres" @@ -37,9 +38,9 @@ POSTGRES_PORT = 5432 # Redis Docker Compose Config REDIS_URL = "redis://redis:6379" -# Etcd Docker Compose Config -ETCD_HOST = "etcd" -ETCD_PORT = 2379 +# Model Discovery Redis Docker Compose Config +DISCOVERY_HOST = "redis" +DISCOVERY_PORT = 6379 # Grafana Docker Compose Config GF_SECURITY_ADMIN_USER = "admin" diff --git a/.github/scripts/update_version.py b/.github/scripts/update_version.py new file mode 100644 index 00000000..cba88e3b --- /dev/null +++ b/.github/scripts/update_version.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +""" +Script to automatically increment Test PyPI package versions. + +This script fetches the latest version from Test PyPI, increments the alpha version, +and updates the pyproject.toml file accordingly. +""" + +import requests +import re +import sys +from pathlib import Path + + +def get_latest_version(package_name="nilai-py"): + """ + Fetch the latest version from Test PyPI. + + Args: + package_name: Name of the package to check + + Returns: + str: Latest version string, or "0.0.0a0" if package doesn't exist + """ + try: + response = requests.get( + f"https://test.pypi.org/pypi/{package_name}/json", timeout=10 + ) + if response.status_code == 404: + # Package doesn't exist yet, start with 0.0.0a1 + print(f"Package {package_name} not found on Test PyPI, starting fresh") + return "0.0.0a0" + + response.raise_for_status() + data = response.json() + versions = list(data["releases"].keys()) + + if not versions: + print("No versions found, starting fresh") + return "0.0.0a0" + + # Filter for alpha versions and find the latest + alpha_versions = [v for v in versions if "a" in v] + if not alpha_versions: + print("No alpha versions found, starting fresh") + return "0.0.0a0" + + # Sort versions and get the latest + alpha_versions.sort(key=lambda x: [int(i) for i in re.findall(r"\d+", x)]) + latest = alpha_versions[-1] + print(f"Found latest alpha version: {latest}") + return latest + + except Exception as e: + print(f"Error fetching version: {e}") + return "0.0.0a0" + + +def increment_version(version): + """ + Increment the alpha version number. + + Args: + version: Version string like "0.0.0a1" + + Returns: + str: Incremented version string like "0.0.0a2" + """ + # Parse version like "0.0.0a1" or "0.1.0a5" + match = re.match(r"(\d+)\.(\d+)\.(\d+)a(\d+)", version) + if match: + major, minor, patch, alpha = match.groups() + new_alpha = int(alpha) + 1 + new_version = f"{major}.{minor}.{patch}a{new_alpha}" + print(f"Incrementing {version} -> {new_version}") + return new_version + else: + # If no match, start with a1 + print(f"Could not parse version {version}, defaulting to 0.0.0a1") + return "0.0.0a1" + + +def update_pyproject_version(new_version, pyproject_path="pyproject.toml"): + """ + Update the version in pyproject.toml file. + + Args: + new_version: New version string to set + pyproject_path: Path to pyproject.toml file + + Returns: + str: The new version that was set + """ + pyproject_file = Path(pyproject_path) + + if not pyproject_file.exists(): + raise FileNotFoundError(f"Could not find {pyproject_path}") + + content = pyproject_file.read_text() + + # Update version line + updated_content = re.sub( + r'^version = ".*"', f'version = "{new_version}"', content, flags=re.MULTILINE + ) + + if content == updated_content: + print("Warning: No version line found to update in pyproject.toml") + + pyproject_file.write_text(updated_content) + print(f"Updated {pyproject_path} with version {new_version}") + return new_version + + +def main(): + """Main function to orchestrate version update.""" + print("=== Updating package version ===") + + # Get latest version from Test PyPI + latest_version = get_latest_version() + print(f"Latest version from Test PyPI: {latest_version}") + + # Increment version + new_version = increment_version(latest_version) + print(f"New version: {new_version}") + + # Update pyproject.toml + update_pyproject_version(new_version) + + # Output for GitHub Actions (using newer syntax) + print(f"NEW_VERSION={new_version}") + + return new_version + + +if __name__ == "__main__": + try: + version = main() + sys.exit(0) + except Exception as e: + print(f"Error: {e}") + sys.exit(1) diff --git a/.github/scripts/update_version_from_release.py b/.github/scripts/update_version_from_release.py new file mode 100644 index 00000000..f6b902ae --- /dev/null +++ b/.github/scripts/update_version_from_release.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +Script to update pyproject.toml version based on GitHub release tag. + +This script takes a release tag (like 'v1.0.0' or '1.0.0') and updates +the version field in pyproject.toml accordingly. +""" + +import re +import sys +import argparse +from pathlib import Path + + +def normalize_version(tag_version): + """ + Normalize a version tag to a clean version string. + + Args: + tag_version: Version from GitHub release tag (e.g., 'v1.0.0', '1.0.0', 'v1.0.0-beta.1') + + Returns: + str: Clean version string (e.g., '1.0.0', '1.0.0b1') + """ + # Remove 'v' prefix if present + version = tag_version.lstrip("v") + + # Convert beta/alpha/rc notation to PEP 440 format + # v1.0.0-beta.1 -> 1.0.0b1 + # v1.0.0-alpha.2 -> 1.0.0a2 + # v1.0.0-rc.1 -> 1.0.0rc1 + version = re.sub(r"-beta\.?(\d+)", r"b\1", version) + version = re.sub(r"-alpha\.?(\d+)", r"a\1", version) + version = re.sub(r"-rc\.?(\d+)", r"rc\1", version) + + print(f"Normalized version: {tag_version} -> {version}") + return version + + +def validate_version(version): + """ + Validate that the version follows PEP 440 format. + + Args: + version: Version string to validate + + Returns: + bool: True if valid, False otherwise + """ + # Basic PEP 440 version pattern + pattern = r"^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$" + + if re.match(pattern, version): + print(f"Version {version} is valid") + return True + else: + print(f"Warning: Version {version} may not be PEP 440 compliant") + return False + + +def update_pyproject_version(new_version, pyproject_path="pyproject.toml"): + """ + Update the version in pyproject.toml file. + + Args: + new_version: New version string to set + pyproject_path: Path to pyproject.toml file + + Returns: + str: The new version that was set + """ + pyproject_file = Path(pyproject_path) + + if not pyproject_file.exists(): + raise FileNotFoundError(f"Could not find {pyproject_path}") + + content = pyproject_file.read_text() + original_content = content + + # Update version line + updated_content = re.sub( + r'^version = ".*"', f'version = "{new_version}"', content, flags=re.MULTILINE + ) + + if content == updated_content: + raise ValueError("No version line found to update in pyproject.toml") + + pyproject_file.write_text(updated_content) + print(f"Updated {pyproject_path} with version {new_version}") + + # Show the change + old_version_match = re.search(r'^version = "(.*)"', original_content, re.MULTILINE) + if old_version_match: + old_version = old_version_match.group(1) + print(f"Version changed: {old_version} -> {new_version}") + + return new_version + + +def main(): + """Main function to orchestrate version update from release tag.""" + parser = argparse.ArgumentParser( + description="Update pyproject.toml version from GitHub release tag" + ) + parser.add_argument( + "tag_version", help="The release tag version (e.g., 'v1.0.0' or '1.0.0')" + ) + parser.add_argument( + "--pyproject", default="pyproject.toml", help="Path to pyproject.toml file" + ) + parser.add_argument( + "--validate", action="store_true", help="Validate version format" + ) + + args = parser.parse_args() + + print("=== Updating version from release tag ===") + print(f"Release tag: {args.tag_version}") + + # Normalize the version + normalized_version = normalize_version(args.tag_version) + + # Validate if requested + if args.validate: + validate_version(normalized_version) + + # Update pyproject.toml + try: + update_pyproject_version(normalized_version, args.pyproject) + print(f"SUCCESS: Updated version to {normalized_version}") + + # Output for GitHub Actions + print(f"RELEASE_VERSION={normalized_version}") + + return 0 + except Exception as e: + print(f"ERROR: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/wait_for_ci_services.sh b/.github/scripts/wait_for_ci_services.sh similarity index 100% rename from scripts/wait_for_ci_services.sh rename to .github/scripts/wait_for_ci_services.sh diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 99857cc6..943f988a 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -11,10 +11,14 @@ on: permissions: id-token: write # Required for OIDC contents: read # Required for checkout + packages: read # Required for GHCR access jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + test-type: [pyright, unit, integration] steps: - uses: actions/checkout@v4 @@ -39,9 +43,11 @@ jobs: uv sync - name: Run Ruff format check + if: matrix.test-type == 'pyright' run: uv run ruff format --check - name: Run Ruff linting + if: matrix.test-type == 'pyright' run: uv run ruff check --exclude packages/verifier/ - name: Create .env for tests @@ -52,13 +58,16 @@ jobs: sed -i 's/BRAVE_SEARCH_API=.*/BRAVE_SEARCH_API=dummy_api/' .env sed -i 's/E2B_API_KEY=.*/E2B_API_KEY=dummy_token/' .env - - name: pyright + - name: Run pyright + if: matrix.test-type == 'pyright' run: uv run pyright - name: Run unit tests + if: matrix.test-type == 'unit' run: uv run pytest -v tests/unit - name: Run integration tests + if: matrix.test-type == 'integration' run: uv run pytest -v tests/integration start-runner: @@ -106,11 +115,26 @@ jobs: runs-on: ${{ needs.start-runner.outputs.label }} strategy: matrix: - component: [vllm, attestation, api] + component: [vllm, api] include: - component: api build_args: "--target nilai --platform linux/amd64" steps: + - name: Disable unattended-upgrades + run: | + echo "Disabling unattended-upgrades to prevent dpkg lock issues..." + # Stop and disable the unattended-upgrades service + sudo systemctl stop unattended-upgrades || true + sudo systemctl disable unattended-upgrades || true + sudo systemctl mask unattended-upgrades || true + # Kill any running unattended-upgrades processes + sudo killall -9 unattended-upgrade apt apt-get dpkg || true + # Remove any stale locks + sudo rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock /var/lib/apt/lists/lock || true + # Reconfigure dpkg in case it was interrupted + sudo dpkg --configure -a || true + echo "unattended-upgrades disabled successfully" + - name: Checkout uses: actions/checkout@v2 @@ -149,6 +173,13 @@ jobs: sed -i 's/NILDB_BUILDER_PRIVATE_KEY=.*/NILDB_BUILDER_PRIVATE_KEY=${{ secrets.NILDB_BUILDER_PRIVATE_KEY }}/' .env sed -i 's/NILDB_COLLECTION=.*/NILDB_COLLECTION=${{ secrets.NILDB_COLLECTION }}/' .env + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GH_PAT }} + - name: Compose docker-compose.yml run: python3 ./scripts/docker-composer.py --dev -f docker/compose/docker-compose.gpt-20b-gpu.ci.yml -o development-compose.yml @@ -250,7 +281,7 @@ jobs: docker ps -a - name: Wait for services to be healthy - run: bash scripts/wait_for_ci_services.sh + run: bash .github/scripts/wait_for_ci_services.sh - name: Run E2E tests for NUC run: | @@ -263,7 +294,7 @@ jobs: run: | set -e # Create a user with a rate limit of 1000 requests per minute, hour, and day - export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 | jq ".apikey" -r) + export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 --apikey SecretTestApiKey | jq ".apikey" -r) export ENVIRONMENT=ci # Set the environment variable for the API key export AUTH_STRATEGY=api_key @@ -280,7 +311,7 @@ jobs: if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'release' strategy: matrix: - component: [vllm, attestation, api] + component: [vllm, api] steps: - name: Configure AWS credentials for ECR uses: aws-actions/configure-aws-credentials@v4 diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 00000000..8dd7ce2d --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,69 @@ +name: Publish nilai-py to PyPI + +on: + release: + types: [published] + workflow_dispatch: # Allow manual trigger for testing + +jobs: + pypi-publish: + name: Publish nilai-py to PyPI + runs-on: ubuntu-latest + + # Only run on published releases (not drafts) and only for nilai-py releases + if: github.event.release.draft == false && startsWith(github.event.release.tag_name, 'nilai-py-v') + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + run: uv python install + + - name: Install dependencies + working-directory: clients/nilai-py + run: uv sync --all-extras --dev + + - name: Run tests + working-directory: clients/nilai-py + run: uv run pytest tests/ + + - name: Update version from release tag + id: version + working-directory: clients/nilai-py + run: | + # Get the release tag (remove refs/tags/ prefix if present) + RELEASE_TAG="${{ github.event.release.tag_name }}" + echo "Release tag: $RELEASE_TAG" + + # Update pyproject.toml with the release version + RELEASE_VERSION=$(uv run python ../../.github/scripts/update_version_from_release.py "$RELEASE_TAG" --validate | grep "RELEASE_VERSION=" | cut -d'=' -f2) + echo "release_version=$RELEASE_VERSION" >> $GITHUB_OUTPUT + echo "Updated version to: $RELEASE_VERSION" + + - name: Verify version update + working-directory: clients/nilai-py + run: | + # Show the updated version in pyproject.toml + grep "^version = " pyproject.toml + echo "Building package with version: ${{ steps.version.outputs.release_version }}" + + - name: Build package + working-directory: clients/nilai-py + run: uv build + + - name: Publish to PyPI + working-directory: clients/nilai-py + env: + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + run: | + echo "Publishing to PyPI..." + uv publish + + - name: Create GitHub release comment + if: success() + run: | + echo "āœ… Successfully published nilai-py v${{ steps.version.outputs.release_version }} to PyPI!" >> $GITHUB_STEP_SUMMARY + echo "šŸ“¦ Package: https://pypi.org/project/nilai-py/${{ steps.version.outputs.release_version }}/" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test-pypi-publish.yml b/.github/workflows/test-pypi-publish.yml new file mode 100644 index 00000000..1841af99 --- /dev/null +++ b/.github/workflows/test-pypi-publish.yml @@ -0,0 +1,51 @@ +name: Publish nilai-py to Test PyPI + +on: + push: + branches: [ main ] + paths: + - 'clients/nilai-py/**' + workflow_dispatch: # Allow manual trigger + +jobs: + test-pypi-publish: + name: Publish nilai-py to Test PyPI + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + run: uv python install + + - name: Install dependencies + working-directory: clients/nilai-py + run: uv sync --all-extras --dev + + - name: Run tests + working-directory: clients/nilai-py + run: uv run pytest tests/ + + - name: Get latest version from Test PyPI and increment + id: version + working-directory: clients/nilai-py + run: | + # Install requests for API calls + uv add --dev requests + + # Run the version update script + NEW_VERSION=$(uv run python ../../.github/scripts/update_version.py | grep "NEW_VERSION=" | cut -d'=' -f2) + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Build package + working-directory: clients/nilai-py + run: uv build + + - name: Publish to Test PyPI + working-directory: clients/nilai-py + env: + UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} + run: uv publish --publish-url https://test.pypi.org/legacy/ diff --git a/.gitignore b/.gitignore index f3d8ab42..7cbf1bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,5 @@ private_key.key.lock development-compose.yml production-compose.yml + +.vscode/ diff --git a/README.md b/README.md index 539759a8..c78b943f 100644 --- a/README.md +++ b/README.md @@ -162,60 +162,6 @@ docker compose -f production-compose.yml up -d docker compose -f production-compose.yml logs -f ``` -### 3. Manual Component Deployment - -#### Components - -- **API Frontend**: Handles user requests and routes model interactions -- **Databases**: - - **SQLite**: User registry and access management - - **etcd3**: Distributed key-value store for model lifecycle management - -#### Setup Steps - -1. **Start etcd3 Instance** - ```shell - docker run -d --name etcd-server \ - -p 2379:2379 -p 2380:2380 \ - -e ALLOW_NONE_AUTHENTICATION=yes \ - bitnami/etcd:latest - - docker run -d --name redis \ - -p 6379:6379 \ - redis:latest - ``` - -2. **Start PostgreSQL** - ```shell - docker run -d --name postgres \ - -e POSTGRES_USER=${POSTGRES_USER} \ - -e POSTGRES_PASSWORD=${POSTGRES_PASSWORD} \ - -e POSTGRES_DB=${POSTGRES_DB} \ - -p 5432:5432 \ - --network frontend_net \ - --volume postgres_data:/var/lib/postgresql/data \ - postgres:16 - ``` - -2. **Run API Server** - ```shell - # Development Environment - fastapi dev nilai-api/src/nilai_api/__main__.py --port 8080 - - # Production Environment - uv run fastapi run nilai-api/src/nilai_api/__main__.py --port 8080 - ``` - -3. **Run Model Instances** - ```shell - # Example: Llama 3.2 1B Model - # Development Environment - uv run fastapi dev nilai-models/src/nilai_models/models/llama_1b_cpu/__init__.py - - # Production Environment - uv run fastapi run nilai-models/src/nilai_models/models/llama_1b_cpu/__init__.py - ``` - ## Developer Workflow ### Code Quality and Formatting @@ -228,7 +174,7 @@ uv run pre-commit install ## Model Lifecycle Management -- Models register themselves in the etcd database +- Models register themselves in the Redis Discovery database - Registration includes address information with an auto-expiring lifetime - If a model disconnects, it is automatically removed from the available models diff --git a/caddy/Caddyfile.http b/caddy/Caddyfile.http index 3fe8d55d..30c76fa8 100644 --- a/caddy/Caddyfile.http +++ b/caddy/Caddyfile.http @@ -6,11 +6,6 @@ # Use :80 explicitly to force HTTP-only behavior :80 { - handle_path /grafana/* { - uri strip_prefix /grafana - reverse_proxy grafana:3000 - } - handle_path /nuc/* { uri strip_prefix /nuc reverse_proxy nilai-nuc-api:8080 diff --git a/clients/nilai-py/.gitignore b/clients/nilai-py/.gitignore new file mode 100644 index 00000000..d34702c6 --- /dev/null +++ b/clients/nilai-py/.gitignore @@ -0,0 +1,178 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +*.sqlite +.ruff_cache/ +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ +bench/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +.DS_Store +verifier.lock + +grafana/runtime-data/* +!grafana/runtime-data/dashboards + +prometheus/data/* +!prometheus/data/.gitkeep + +private_key.key +private_key.key.lock + +keys/* +stored_prompts/* diff --git a/clients/nilai-py/.python-version b/clients/nilai-py/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/clients/nilai-py/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/clients/nilai-py/AGENTS.md b/clients/nilai-py/AGENTS.md new file mode 100644 index 00000000..7a39d28d --- /dev/null +++ b/clients/nilai-py/AGENTS.md @@ -0,0 +1,25 @@ +# Repository Guidelines + +## Project Structure & Module Organization +Source code lives under `src/nilai_py/`, with `client.py` exposing the OpenAI-compatible client, `server.py` managing delegation tokens, and `niltypes.py` centralizing typed models. Tests reside in `tests/` and mirror runtime modules (`test_server.py`, `test_nilai_openai.py`). Sample workflows sit in `examples/`, reusable prompt templates in `stored_prompts/`, and build artifacts land in `dist/`. Keep configuration in `.env` files or the `keys/` directory; do not commit secrets. + +## Build, Test, and Development Commands +- `uv sync` — install runtime dependencies declared in `pyproject.toml`. +- `uv sync --group dev` — install tooling for linting and tests. +- `uv run pytest` — execute the full test suite. +- `uv run pytest tests/test_server.py -v` — target a specific module with verbose output. +- `uv run pytest --cov=nilai_py --cov-report=term-missing` — check coverage before submitting changes. +- `uv run ruff check` / `uv run ruff format` — lint and auto-format to project standards. +- `uv build` — produce distributable wheels and source archives. + +## Coding Style & Naming Conventions +Stick to Python 3.12 standards with 4-space indentation, type hints, and explicit docstrings where behavior is non-trivial. Use snake_case for functions and variables, PascalCase for classes, and UPPER_CASE for module-level constants. Align imports per Ruff ordering, keep modules focused, and co-locate helper functions with their primary caller to ease review. + +## Testing Guidelines +Write tests with `pytest`, placing files in `tests/` using the `test_*.py` pattern and descriptive method names. Mock external network calls so suites run offline. Maintain or improve the current ~70% coverage by running `uv run pytest --cov=nilai_py --cov-report=term-missing` and addressing gaps surfaced in `term-missing` output. When adding integrations, extend `test_nilai_openai.py`; for delegation server logic, update `test_server.py`. + +## Commit & Pull Request Guidelines +Follow Conventional Commit prefixes observed in the history (`feat:`, `fix:`, `docs:`, etc.) and keep subjects under 72 characters. Each pull request should: 1) summarize intent and key changes, 2) link related issues or tickets, 3) note test evidence (command output or coverage delta), and 4) include screenshots or logs when altering user-visible behavior. Request review only after lint and tests pass locally. + +## Security & Configuration Tips +Load credentials via environment variables or `.env` files and treat `keys/` artifacts as local-only. When sharing examples, redact API keys and private keys. Default endpoints in the SDK point to sandbox infrastructure; document any production overrides in your PR description. diff --git a/clients/nilai-py/CLAUDE.md b/clients/nilai-py/CLAUDE.md new file mode 100644 index 00000000..9d4bc9f4 --- /dev/null +++ b/clients/nilai-py/CLAUDE.md @@ -0,0 +1,175 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Development Setup +```bash +# Install dependencies (uses uv for dependency management) +uv sync + +# Install with development dependencies +uv sync --group dev +``` + +### Testing +```bash +# Run all tests +uv run pytest + +# Run tests with coverage +uv run pytest --cov=nilai_py --cov-report=term-missing + +# Run specific test file +uv run pytest tests/test_server.py + +# Run specific test class +uv run pytest tests/test_server.py::TestDelegationTokenServer -v + +# Run specific test method +uv run pytest tests/test_server.py::TestDelegationTokenServer::test_create_delegation_token_success -v +``` + +### Code Quality +```bash +# Run linter and formatter +uv run ruff check +uv run ruff format +``` + +### Running Examples +```bash +# Examples are located in the examples/ directory +python examples/0-api_key_mode.py +python examples/1-delegation_token_mode.py +python examples/2-streaming_mode.py +python examples/3-advanced_streaming.py +python examples/4-concurrent-streaming.py +python examples/5-nildb-prompt-storage.py +python examples/6-nildb-stored-prompt.py +python examples/7-web-search.py +``` + +## Architecture + +### Core Components + +**Client (`src/nilai_py/client.py`)** +- OpenAI-compatible client extending `openai.Client` +- Supports two authentication modes: API_KEY and DELEGATION_TOKEN +- Handles NUC token creation and Nilai-specific authentication headers +- Manages root tokens (API key mode) and delegation tokens automatically + +**DelegationTokenServer (`src/nilai_py/server.py`)** +- Server-side component for creating delegation tokens +- Manages root token lifecycle with automatic refresh on expiration +- Creates time-limited delegation tokens with configurable usage limits +- Handles NilAuth integration for secure token generation + +**NilDB Prompt Manager (`src/nilai_py/nildb/__init__.py`)** +- Document management system for handling prompts in NilDB +- User setup and key management with SecretVaults integration +- CRUD operations for documents with delegation token authentication + +### Authentication Flow + +1. **API Key Mode**: Direct authentication using API key from nilpay.vercel.app + - Client initializes with API key, creates root token via NilAuth + - Root token is cached and auto-refreshed when expired + - Invocation tokens created from root token for each request + +2. **Delegation Token Mode**: Server-side token generation for enhanced security + - Client generates temporary keypair and requests delegation + - Server creates delegation token using its root token + - Client uses delegation token to create invocation tokens for requests + +### Key Dependencies + +- `nuc`: NUC token creation and envelope handling +- `openai`: Base OpenAI client functionality +- `secretvaults`: Secure key storage for NilDB operations +- `httpx`: HTTP client for Nilai API communication +- `pydantic`: Data validation and serialization + +### Configuration + +**Environment Variables** +- `API_KEY`: API key for direct authentication mode +- `PRIVATE_KEY`: Server private key for delegation token creation + +**NilAuth Instances** +- `SANDBOX`: https://nilauth.sandbox.app-cluster.sandbox.nilogy.xyz +- `PRODUCTION`: https://nilauth-cf7f.nillion.network/ + +### Testing Structure + +- `tests/unit/`: Unit tests for individual components +- `tests/e2e/`: End-to-end integration tests +- Test coverage focused on DelegationTokenServer (100% coverage) and core functionality + +### Examples Structure + +The examples directory demonstrates various SDK capabilities: + +- `examples/0-api_key_mode.py`: Basic API key authentication +- `examples/1-delegation_token_mode.py`: Delegation token flow +- `examples/2-streaming_mode.py`: Basic streaming responses +- `examples/3-advanced_streaming.py`: Advanced streaming with error handling +- `examples/4-concurrent-streaming.py`: Multiple concurrent streaming requests +- `examples/5-nildb-prompt-storage.py`: Storing prompts in NilDB with delegation +- `examples/6-nildb-stored-prompt.py`: Using stored prompts with complex delegation chains +- `examples/7-web-search.py`: Web search capabilities + +### NilDB Integration + +The SDK includes a complete document management system (`src/nilai_py/nildb/`) for handling prompts: + +- **Document Operations**: Create, list, and manage prompt documents +- **User Management**: Automatic user setup with SecretVaults integration +- **Delegation Chain**: Complex delegation token flows for document access +- **Key Components**: + - `NilDBPromptManager`: Main interface for document operations + - `UserSetupResult`: User configuration and key management + - `DocumentReference`: Document metadata and access control + +### Streaming Support + +Both authentication modes support real-time streaming responses with: +- Real-time chunk processing +- Progress tracking and monitoring +- Error handling and retry logic +- Concurrent streaming capabilities + +## Development Patterns + +### File Structure Conventions +- Core functionality in `src/nilai_py/`: Client, server, and type definitions +- NilDB subsystem in `src/nilai_py/nildb/`: Document management and user operations +- Examples in `examples/`: Numbered examples with specific use cases +- Tests split between `tests/unit/` and `tests/e2e/` + +### Authentication Architecture +The SDK uses a two-tier authentication system: +1. **Root Tokens**: Long-lived server credentials (API key or private key) +2. **Delegation/Invocation Tokens**: Short-lived request tokens + +**Token Flow**: +- Server creates root token using NilAuth +- Root token generates delegation tokens with configurable expiration/usage limits +- Client uses delegation tokens to create invocation tokens for each API request +- All tokens automatically refresh when expired + +### NilDB Document Flow +Complex delegation chains for document access: +1. **Subscription Owner Server**: Controls API access using API key +2. **Prompt Data Owner Server**: Controls document access using private key +3. **Client**: Makes requests using chained delegation tokens + +This enables fine-grained access control where document owners can delegate access independently of API subscription owners. + +# important-instruction-reminders +Do what has been asked; nothing more, nothing less. +NEVER create files unless they're absolutely necessary for achieving your goal. +ALWAYS prefer editing an existing file to creating a new one. +NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. diff --git a/clients/nilai-py/README.md b/clients/nilai-py/README.md new file mode 100644 index 00000000..8be3dccd --- /dev/null +++ b/clients/nilai-py/README.md @@ -0,0 +1,307 @@ +# Nilai Python SDK + +A Python SDK for the Nilai platform that provides delegation token management and OpenAI-compatible client functionality for accessing AI models through secure, decentralized infrastructure. + +## šŸš€ Quick Start + +### Installation + +This project uses [uv](https://docs.astral.sh/uv/) for dependency management. + +```bash +# Install dependencies +uv sync + +# Install with development dependencies +uv sync --group dev +``` + +### Basic Usage + +```python +from nilai_py import Client + +# Initialize client with API key +client = Client( + base_url="https://testnet-p0.nilai.sandbox.nilogy.xyz/nuc/v1/", + api_key="your-api-key-here" +) + +# Make a chat completion request +response = client.chat.completions.create( + model="meta-llama/Llama-3.2-3B-Instruct", + messages=[ + {"role": "user", "content": "Hello! Can you help me with something?"} + ], +) + +print(f"Response: {response.choices[0].message.content}") +``` + +## šŸ“– Usage Examples + +### 1. API Key Mode (Simple) + +The easiest way to get started. You'll need an API key from [nilpay.vercel.app](https://nilpay.vercel.app/). + +```python +from nilai_py import Client + +# Set up your API key in a .env file or environment variable +client = Client( + base_url="https://testnet-p0.nilai.sandbox.nilogy.xyz/nuc/v1/", + api_key="your-api-key-here" +) + +# Make requests just like with OpenAI +response = client.chat.completions.create( + model="meta-llama/Llama-3.2-3B-Instruct", + messages=[ + {"role": "user", "content": "Explain quantum computing in simple terms"} + ], +) + +print(response.choices[0].message.content) +``` + +### 2. Delegation Token Mode (Advanced) + +For more secure, distributed access where you want to separate server credentials from client usage. + +```python +from nilai_py import ( + Client, + DelegationTokenServer, + AuthType, + DelegationServerConfig, + NilAuthInstance +) + +# Server-side: Create a delegation token server +server = DelegationTokenServer( + private_key="your-private-key", + config=DelegationServerConfig( + nilauth_url=NilAuthInstance.SANDBOX.value, + expiration_time=3600, # 1 hour validity + token_max_uses=10, # Allow 10 uses + ) +) + +# Client-side: Initialize client for delegation token mode +client = Client( + base_url="https://nilai-a779.nillion.network/nuc/v1/", + auth_type=AuthType.DELEGATION_TOKEN, +) + +# Step 1: Client requests delegation +delegation_request = client.get_delegation_request() + +# Step 2: Server creates delegation token +delegation_token = server.create_delegation_token(delegation_request) + +# Step 3: Client uses the delegation token +client.update_delegation(delegation_token) + +# Step 4: Make authenticated requests +response = client.chat.completions.create( + model="meta-llama/Llama-3.2-3B-Instruct", + messages=[ + {"role": "user", "content": "What are the benefits of decentralized AI?"} + ], +) + +print(response.choices[0].message.content) +``` + +### 3. Environment Configuration + +Create a `.env` file for your credentials: + +```bash +# .env file +API_KEY=your-api-key-from-nilpay +PRIVATE_KEY=your-private-key-for-delegation-tokens +``` + +Then in your code: + +```python +import os +from dotenv import load_dotenv +from nilai_py import Client + +load_dotenv() + +client = Client( + base_url="https://testnet-p0.nilai.sandbox.nilogy.xyz/nuc/v1/", + api_key=os.getenv("API_KEY") +) +``` + +## ✨ Features + +- **šŸ” Multiple Authentication Methods**: Support for API keys and delegation tokens +- **šŸ¤– OpenAI Compatibility**: Drop-in replacement for OpenAI client in most cases +- **⚔ Automatic Token Management**: Handles token caching and expiration automatically +- **šŸ›”ļø Secure Delegation**: Server-side token management with configurable expiration and usage limits +- **🌐 Network Flexibility**: Support for sandbox and production environments +- **šŸ“ Type Safety**: Full TypeScript-style type annotations for better IDE support + +## šŸ—ļø Architecture + +### DelegationTokenServer +Server-side component responsible for: +- Creating delegation tokens with configurable expiration and usage limits +- Managing root token lifecycle and caching +- Handling cryptographic operations securely + +### Client +OpenAI-compatible client that: +- Supports both API key and delegation token authentication +- Automatically handles NUC token creation and management +- Provides familiar chat completion interface + +### Token Management +- **Root Tokens**: Long-lived tokens for server authentication +- **Delegation Tokens**: Short-lived, limited-use tokens for client operations +- **Automatic Refresh**: Expired tokens are automatically refreshed when needed + +## Features + +- **DelegationTokenServer**: Server-side delegation token management +- **Client**: OpenAI-compatible client with Nilai authentication +- **Token Management**: Automatic token caching and expiration handling +- **Multiple Auth Methods**: Support for API keys and delegation tokens + +## Testing + +### Running Tests + +To run all tests: +```bash +uv run pytest +``` + +To run tests for a specific module (e.g., server tests): +```bash +uv run pytest tests/test_server.py +``` + +To run tests with verbose output: +```bash +uv run pytest -v +``` + +To run tests for a specific test class: +```bash +uv run pytest tests/test_server.py::TestDelegationTokenServer -v +``` + +To run a specific test method: +```bash +uv run pytest tests/test_server.py::TestDelegationTokenServer::test_create_delegation_token_success -v +``` + +### Test Coverage + +To run tests with coverage reporting: +```bash +uv run pytest --cov=nilai_py --cov-report=term-missing +``` + +To generate an HTML coverage report: +```bash +uv run pytest --cov=nilai_py --cov-report=html +``` + +### Current Test Coverage + +The test suite provides comprehensive coverage: + +| Module | Coverage | Details | +|--------|----------|---------| +| `src/nilai_py/server.py` | **100%** | Complete coverage of DelegationTokenServer class | +| `src/nilai_py/niltypes.py` | **100%** | Complete coverage of type definitions | +| `src/nilai_py/__init__.py` | **100%** | Module initialization | +| **Overall** | **71%** | High coverage across tested modules | + +#### DelegationTokenServer Tests (16 test cases) + +The `DelegationTokenServer` class has comprehensive test coverage including: + +- āœ… **Initialization**: Default and custom configurations, invalid key handling +- āœ… **Token Expiration**: Expired/valid token detection, no expiration handling +- āœ… **Root Token Management**: Caching, automatic refresh, first access +- āœ… **Delegation Token Creation**: Success cases, configuration overrides, error handling +- āœ… **Error Handling**: Network failures, invalid cryptographic keys +- āœ… **Configuration**: Property access and instance management + +### Test Structure + +``` +tests/ +ā”œā”€ā”€ test_server.py # DelegationTokenServer tests (100% coverage) +ā”œā”€ā”€ test_nilai_openai.py # Client integration tests +└── config.py # Test configuration +``` + +### Running Tests in Development + +For continuous testing during development: +```bash +# Watch for file changes and rerun tests +uv run pytest --watch +``` + +### Test Dependencies + +The following testing dependencies are included in the `dev` group: +- `pytest>=8.4.0`: Test framework +- `pytest-cov>=6.2.1`: Coverage reporting + +## Development + +### Code Quality + +Run linting with: +```bash +uv run ruff check +uv run ruff format +``` + +### Adding New Tests + +When adding new functionality: +1. Create corresponding test files in the `tests/` directory +2. Follow the existing naming convention (`test_*.py`) +3. Use descriptive test method names +4. Include docstrings explaining test purposes +5. Mock external dependencies appropriately +6. Aim for high test coverage + +### Example Test Command Workflow + +```bash +# 1. Install dependencies +uv sync --group dev + +# 2. Run all tests with coverage +uv run pytest --cov=nilai_py --cov-report=term-missing + +# 3. Run specific module tests +uv run pytest tests/test_server.py -v + +# 4. Check code quality +uv run ruff check +uv run ruff format +``` + +## Project Structure + +``` +src/nilai_py/ +ā”œā”€ā”€ __init__.py # Package initialization +ā”œā”€ā”€ client.py # OpenAI-compatible client +ā”œā”€ā”€ server.py # DelegationTokenServer class +└── niltypes.py # Type definitions +``` diff --git a/clients/nilai-py/examples/0-api_key_mode.py b/clients/nilai-py/examples/0-api_key_mode.py new file mode 100644 index 00000000..53bfeb32 --- /dev/null +++ b/clients/nilai-py/examples/0-api_key_mode.py @@ -0,0 +1,46 @@ +from nilai_py import Client + +from config import API_KEY +from openai import DefaultHttpxClient + + +def main(): + # Initialize the client in API key mode + # To obtain an API key, navigate to https://nilpay.vercel.app/ + # and create a new subscription. + # The API key will be displayed in the subscription details. + # The Client class automatically handles the NUC token creation and management. + ## For sandbox, use the following: + http_client = DefaultHttpxClient(verify=False) + + # Create the OpenAI client with the custom endpoint and API key + client = Client( + base_url="https://localhost/nuc/v1", + api_key=API_KEY, + http_client=http_client, + # For production, use the following: + # nilauth_instance=NilAuthInstance.PRODUCTION, + ) + + # Make a request to the Nilai API + response = client.chat.completions.create( + model="openai/gpt-oss-20b", + messages=[ + { + "role": "user", + "content": "Create a story written as if you were a pirate. Write in a pirate accent.", + } + ], + stream=True, + ) + + for chunk in response: + if chunk.choices[0].finish_reason is not None: + print("\n[DONE]") + break + if chunk.choices[0].delta.content is not None: + print(chunk.choices[0].delta.content, end="", flush=True) + + +if __name__ == "__main__": + main() diff --git a/clients/nilai-py/examples/1-delegation_token_mode.py b/clients/nilai-py/examples/1-delegation_token_mode.py new file mode 100644 index 00000000..3c606de5 --- /dev/null +++ b/clients/nilai-py/examples/1-delegation_token_mode.py @@ -0,0 +1,64 @@ +from openai import DefaultHttpxClient +from nilai_py import ( + Client, + DelegationTokenServer, + AuthType, + DelegationServerConfig, + DelegationTokenRequest, + DelegationTokenResponse, +) + +from config import API_KEY + + +def main(): + # >>> Server initializes a delegation token server + # The server is responsible for creating delegation tokens + # and managing their expiration and usage. + http_client = DefaultHttpxClient(verify=False) + + server = DelegationTokenServer( + private_key=API_KEY, + config=DelegationServerConfig( + expiration_time=10, # 10 seconds validity of delegation tokens + token_max_uses=1, # 1 use of a delegation token + ), + # For production instances, use the following: + # nilauth_instance=NilAuthInstance.PRODUCTION, + ) + + # >>> Client initializes a client + # The client is responsible for making requests to the Nilai API. + # We do not provide an API key but we set the auth type to DELEGATION_TOKEN + client = Client( + base_url="https://localhost/nuc/v1", + auth_type=AuthType.DELEGATION_TOKEN, + http_client=http_client, + # For production instances, use the following: + # nilauth_instance=NilAuthInstance.PRODUCTION, + ) + for i in range(100): + # >>> Client produces a delegation request + delegation_request: DelegationTokenRequest = client.get_delegation_request() + + # <<< Server creates a delegation token + delegation_token: DelegationTokenResponse = server.create_delegation_token( + delegation_request + ) + + # >>> Client sets internally the delegation token + client.update_delegation(delegation_token) + + # >>> Client uses the delegation token to make a request + response = client.chat.completions.create( + model="openai/gpt-oss-20b", + messages=[ + {"role": "user", "content": "Hello! Can you help me with something?"} + ], + ) + + print(f"Response {i}: {response.choices[0].message.content}") + + +if __name__ == "__main__": + main() diff --git a/clients/nilai-py/examples/2-streaming_mode.py b/clients/nilai-py/examples/2-streaming_mode.py new file mode 100644 index 00000000..07301dd1 --- /dev/null +++ b/clients/nilai-py/examples/2-streaming_mode.py @@ -0,0 +1,56 @@ +from nilai_py import Client + +from config import API_KEY + + +def main(): + # Initialize the client in API key mode + # To obtain an API key, navigate to https://nilpay.vercel.app/ + # and create a new subscription. + # The API key will be displayed in the subscription details. + # The Client class automatically handles the NUC token creation and management. + ## For sandbox, use the following: + client = Client( + base_url="https://nilai-a779.nillion.network/v1/", + api_key=API_KEY, + # For production, use the following: + # nilauth_instance=NilAuthInstance.PRODUCTION, + ) + + # Make a streaming request to the Nilai API + print("Starting streaming response...") + print("=" * 50) + + stream = client.chat.completions.create( + model="google/gemma-3-27b-it", + messages=[ + { + "role": "user", + "content": "Write a short story about a robot learning to paint. Make it creative and engaging.", + } + ], + stream=True, # Enable streaming + ) + + # Process the streaming response + full_response = "" + for chunk in stream: + if ( + chunk.choices is not None + and len(chunk.choices) > 0 + and chunk.choices[0].delta.content is not None + ): + content = chunk.choices[0].delta.content + print( + content, end="", flush=True + ) # Print without newline and flush immediately + full_response += content + + print("\n" + "=" * 50) + print( + f"\nStreaming completed. Full response length: {len(full_response)} characters" + ) + + +if __name__ == "__main__": + main() diff --git a/clients/nilai-py/examples/3-advanced_streaming.py b/clients/nilai-py/examples/3-advanced_streaming.py new file mode 100644 index 00000000..b4a24cc1 --- /dev/null +++ b/clients/nilai-py/examples/3-advanced_streaming.py @@ -0,0 +1,247 @@ +from nilai_py import Client +import time +import threading +import sys +import shutil + +from config import API_KEY + + +class VimStatusBar: + """A true vim-like status bar that stays fixed at the bottom""" + + def __init__(self): + self.stats: StreamingStats | None = None + self.is_running = False + self.thread = None + self.terminal_height = self._get_terminal_height() + + def _get_terminal_height(self): + """Get terminal height""" + try: + return shutil.get_terminal_size().lines + except (OSError, AttributeError): + return 24 # Default fallback + + def start(self, stats): + """Initialize and start the status bar""" + self.stats = stats + self.is_running = True + + # Clear screen and set up scrolling region + sys.stdout.write("\033[2J") # Clear entire screen + sys.stdout.write("\033[H") # Move cursor to top-left + + # Set scrolling region (lines 1 to height-2, leaving last line for status) + if self.terminal_height > 2: + sys.stdout.write(f"\033[1;{self.terminal_height - 1}r") + + sys.stdout.flush() + + # Start status update thread + self.thread = threading.Thread(target=self._status_loop, daemon=True) + self.thread.start() + + def stop(self): + """Stop the status bar and clean up""" + self.is_running = False + if self.thread: + self.thread.join(timeout=0.5) + + # Reset scrolling region + sys.stdout.write("\033[r") + # Clear status line + sys.stdout.write(f"\033[{self.terminal_height};1H\033[K") + sys.stdout.write("\n") + sys.stdout.flush() + + def _status_loop(self): + """Background thread that updates status bar""" + while self.is_running and self.stats: + self._update_status() + time.sleep(0.1) # Update 10 times per second + + def _update_status(self): + """Update the status bar at the bottom""" + if not self.stats: + return + + # Save current cursor position + sys.stdout.write("\033[s") + + # Move to status line and clear it + sys.stdout.write(f"\033[{self.terminal_height};1H\033[K") + + # Write status + status = self._format_status() + sys.stdout.write(status) + + # Restore cursor position + sys.stdout.write("\033[u") + sys.stdout.flush() + + def _format_status(self): + """Format the status string""" + if self.stats is None: + return "" + + elapsed = self.stats.get_elapsed_time() + tokens_per_sec = self.stats.get_tokens_per_second() + chars_per_sec = self.stats.get_chars_per_second() + + # Format elapsed time + if elapsed < 60: + time_str = f"{elapsed:.1f}s" + else: + minutes = int(elapsed // 60) + seconds = elapsed % 60 + time_str = f"{minutes}m{seconds:.1f}s" + + return ( + f"ā±ļø {time_str} | " + f"šŸ”¤ {self.stats.tokens_produced} tokens | " + f"šŸ“ {self.stats.characters_produced} chars | " + f"šŸ“„ {self.stats.lines_produced} lines | " + f"⚔ {tokens_per_sec:.1f} tok/s | " + f"šŸš€ {chars_per_sec:.1f} char/s" + ) + + +class StreamingStats: + def __init__(self): + self.start_time = None + self.tokens_produced = 0 + self.characters_produced = 0 + self.words_produced = 0 + self.lines_produced = 0 + self.current_line = "" + self.is_streaming = False + + def start(self): + self.start_time = time.time() + self.is_streaming = True + + def update(self, content): + self.characters_produced += len(content) + self.tokens_produced += len(content.split()) # Rough token estimation + self.current_line += content + + # Count words (simple whitespace-based counting) + words_in_content = len([w for w in content.split() if w.strip()]) + self.words_produced += words_in_content + + # Count lines + if "\n" in content: + self.lines_produced += content.count("\n") + self.current_line = content.split("\n")[-1] # Keep the current line + + def get_elapsed_time(self): + if self.start_time is None: + return 0 + return time.time() - self.start_time + + def get_tokens_per_second(self): + elapsed = self.get_elapsed_time() + if elapsed == 0: + return 0 + return self.tokens_produced / elapsed + + def get_chars_per_second(self): + elapsed = self.get_elapsed_time() + if elapsed == 0: + return 0 + return self.characters_produced / elapsed + + +def main(): + # Initialize the client in API key mode + # To obtain an API key, navigate to https://nilpay.vercel.app/ + # and create a new subscription. + # The API key will be displayed in the subscription details. + # The Client class automatically handles the NUC token creation and management. + ## For sandbox, use the following: + client = Client( + # base_url="https://nilai-a779.nillion.network/v1/", + api_key=API_KEY, + # For production, use the following: + base_url="https://nilai-f910.nillion.network/nuc/v1/", + ) + + # Initialize statistics tracking and status bar + stats = StreamingStats() + status_bar = VimStatusBar() + + # Start the vim-like status bar + status_bar.start(stats) + + print("šŸš€ Starting streaming response with vim-like status bar...") + print("=" * 80) + print("Press Ctrl+C to interrupt") + print("=" * 80) + print() # Add some space before content starts + + # Make a streaming request to the Nilai API + stream = client.chat.completions.create( + # model="google/gemma-3-27b-it", + # model="openai/gpt-oss-20b", + model="meta-llama/Llama-3.1-8B-Instruct", + messages=[ + { + "role": "user", + "content": "Write a detailed story about a robot learning to paint. Make it creative, engaging, and include dialogue between the robot and its human teacher. The story should be at least 500 words long.", + } + ], + stream=True, # Enable streaming + ) + + # Start statistics tracking + stats.start() + + # Process the streaming response + full_response = "" + try: + for chunk in stream: + if ( + chunk.choices is not None + and len(chunk.choices) > 0 + and chunk.choices[0].delta.content is not None + ): + content = chunk.choices[0].delta.content + + # Update statistics + stats.update(content) + + # Print content normally - status bar handles itself + print(content, end="", flush=True) + full_response += content + + except KeyboardInterrupt: + print("\n\nāš ļø Streaming interrupted by user") + stats.is_streaming = False + status_bar.stop() + return + except Exception as e: + print(f"\n\nāŒ Error during streaming: {e}") + stats.is_streaming = False + status_bar.stop() + return + + # Stop streaming and status bar + stats.is_streaming = False + status_bar.stop() + + # Show final results + print("\n" + "=" * 80) + print("āœ… Streaming completed!") + print("šŸ“Š Final Statistics:") + print(f" ā±ļø Total time: {stats.get_elapsed_time():.2f} seconds") + print(f" šŸ”¤ Total tokens: {stats.tokens_produced}") + print(f" šŸ“ Total characters: {stats.characters_produced}") + print(f" šŸ“„ Total lines: {stats.lines_produced}") + print(f" ⚔ Average tokens/second: {stats.get_tokens_per_second():.2f}") + print(f" šŸš€ Average characters/second: {stats.get_chars_per_second():.2f}") + print("=" * 80) + + +if __name__ == "__main__": + main() diff --git a/clients/nilai-py/examples/4-concurrent-streaming.py b/clients/nilai-py/examples/4-concurrent-streaming.py new file mode 100644 index 00000000..494911af --- /dev/null +++ b/clients/nilai-py/examples/4-concurrent-streaming.py @@ -0,0 +1,310 @@ +from openai import DefaultHttpxClient +from nilai_py import Client +import time +import threading +import sys +import shutil +from concurrent.futures import ThreadPoolExecutor + +from config import API_KEY + + +class VimStatusBar: + """A true vim-like status bar that stays fixed at the bottom""" + + def __init__(self): + self.stats: ConcurrentStreamingStats | None = None + self.is_running = False + self.thread = None + self.terminal_height = self._get_terminal_height() + + def _get_terminal_height(self): + """Get terminal height""" + try: + return shutil.get_terminal_size().lines + except (OSError, AttributeError): + return 24 # Default fallback + + def start(self, stats): + """Initialize and start the status bar""" + self.stats = stats + self.is_running = True + + # Clear screen and set up scrolling region + sys.stdout.write("\033[2J") # Clear entire screen + sys.stdout.write("\033[H") # Move cursor to top-left + + # Set scrolling region (lines 1 to height-2, leaving last line for status) + if self.terminal_height > 2: + sys.stdout.write(f"\033[1;{self.terminal_height - 1}r") + + sys.stdout.flush() + + # Start status update thread + self.thread = threading.Thread(target=self._status_loop, daemon=True) + self.thread.start() + + def stop(self): + """Stop the status bar and clean up""" + self.is_running = False + if self.thread: + self.thread.join(timeout=0.5) + + # Reset scrolling region + sys.stdout.write("\033[r") + # Clear status line + sys.stdout.write(f"\033[{self.terminal_height};1H\033[K") + sys.stdout.write("\n") + sys.stdout.flush() + + def _status_loop(self): + """Background thread that updates status bar""" + while self.is_running and self.stats: + self._update_status() + time.sleep(0.1) # Update 10 times per second + + def _update_status(self): + """Update the status bar at the bottom""" + if not self.stats: + return + + # Save current cursor position + sys.stdout.write("\033[s") + + # Move to status line and clear it + sys.stdout.write(f"\033[{self.terminal_height};1H\033[K") + + # Write status + status = self._format_status() + sys.stdout.write(status) + + # Restore cursor position + sys.stdout.write("\033[u") + sys.stdout.flush() + + def _format_status(self): + """Format the status string""" + if self.stats is None: + return "" + + elapsed = self.stats.get_elapsed_time() + tokens_per_sec = self.stats.get_tokens_per_second() + chars_per_sec = self.stats.get_chars_per_second() + + # Format elapsed time + if elapsed < 60: + time_str = f"{elapsed:.1f}s" + else: + minutes = int(elapsed // 60) + seconds = elapsed % 60 + time_str = f"{minutes}m{seconds:.1f}s" + + # Get stream status + active = getattr(self.stats, "active_streams", 0) + completed = getattr(self.stats, "completed_streams", 0) + total = getattr(self.stats, "total_streams", 1) + + return ( + f"ā±ļø {time_str} | " + f"🌊 {active}/{total} streams | " + f"āœ… {completed} done | " + f"šŸ”¤ {self.stats.tokens_produced} tokens | " + f"šŸ“ {self.stats.characters_produced} chars | " + f"⚔ {tokens_per_sec:.1f} tok/s | " + f"šŸš€ {chars_per_sec:.1f} char/s" + ) + + +class ConcurrentStreamingStats: + def __init__(self): + self.start_time = None + self.tokens_produced = 0 + self.characters_produced = 0 + self.words_produced = 0 + self.lines_produced = 0 + self.is_streaming = False + self.active_streams = 0 + self.completed_streams = 0 + self.total_streams = 0 + self._lock = threading.Lock() # Thread safety for concurrent updates + + def start(self, total_streams=1): + self.start_time = time.time() + self.is_streaming = True + self.total_streams = total_streams + + def start_stream(self): + """Called when a new stream starts""" + with self._lock: + self.active_streams += 1 + + def end_stream(self): + """Called when a stream completes""" + with self._lock: + self.active_streams -= 1 + self.completed_streams += 1 + + def update(self, content, stream_id=None): + """Thread-safe update from any stream""" + with self._lock: + self.characters_produced += len(content) + self.tokens_produced += len(content.split()) # Rough token estimation + + # Count words (simple whitespace-based counting) + words_in_content = len([w for w in content.split() if w.strip()]) + self.words_produced += words_in_content + + # Count lines + if "\n" in content: + self.lines_produced += content.count("\n") + + def get_elapsed_time(self): + if self.start_time is None: + return 0 + return time.time() - self.start_time + + def get_tokens_per_second(self): + elapsed = self.get_elapsed_time() + if elapsed == 0: + return 0 + return self.tokens_produced / elapsed + + def get_chars_per_second(self): + elapsed = self.get_elapsed_time() + if elapsed == 0: + return 0 + return self.characters_produced / elapsed + + +def stream_worker(stream_id, stats, prompts): + """Worker function to handle a single streaming request""" + try: + http_client = DefaultHttpxClient(verify=False) + # Create a separate client for this thread + client = Client( + # base_url="https://nilai-a779.nillion.network/v1/", + api_key=API_KEY, + http_client=http_client, + # For production, use the following: + # base_url="https://nilai-f910.nillion.network/nuc/v1/", + # nilauth_instance=NilAuthInstance.PRODUCTION, + base_url="https://localhost/nuc/v1", + ) + + # Start this stream + stats.start_stream() + + # Select prompt for this stream + prompt = prompts[stream_id % len(prompts)] + + # Make streaming request + stream = client.chat.completions.create( + model="openai/gpt-oss-20b", + messages=[{"role": "user", "content": prompt}], + stream=True, + ) + + stream_response = "" + for chunk in stream: + if ( + chunk.choices is not None + and len(chunk.choices) > 0 + and chunk.choices[0].delta.content is not None + ): + content = chunk.choices[0].delta.content + + # Update global statistics + stats.update(content, stream_id) + + # Print content with stream ID prefix + # print(f"[S{stream_id}] {content}", end="", flush=True) + stream_response += content + + except Exception as e: + print(f"\nāŒ Stream {stream_id} error: {e}") + finally: + # Mark stream as completed + stats.end_stream() + + +def main(): + # Configuration + NUM_CONCURRENT_STREAMS = 3 + + # Different prompts to make streams more interesting + prompts = [ + "Write a story about a robot learning to paint. Include dialogue and make it creative.", + "Create a tale about an AI discovering music. Make it emotional and engaging.", + "Tell a story about a space explorer finding a new planet. Include adventure and wonder.", + "Write about a time traveler visiting ancient civilizations. Make it historically rich.", + "Create a story about underwater creatures building a city. Make it imaginative.", + "Tell a tale about flying cars in a future city. Include technology and human drama.", + "Write about a detective solving mysteries with the help of AI. Make it suspenseful.", + "Create a story about plants that can communicate. Make it scientific yet magical.", + "Tell about a chef creating dishes that evoke memories. Make it sensory and emotional.", + "Write a story about architects designing cities in the clouds. Make it visionary.", + ] + + # Initialize statistics tracking and status bar + stats = ConcurrentStreamingStats() + status_bar = VimStatusBar() + + # Start the vim-like status bar + status_bar.start(stats) + + print("šŸš€ Starting 10 concurrent streaming requests with aggregated stats...") + print("=" * 80) + print("Press Ctrl+C to interrupt all streams") + print("=" * 80) + print() # Add some space before content starts + + # Start statistics tracking + stats.start(NUM_CONCURRENT_STREAMS) + + try: + # Create thread pool and submit all streaming tasks + with ThreadPoolExecutor(max_workers=NUM_CONCURRENT_STREAMS) as executor: + # Submit all streaming tasks + futures = [] + for i in range(NUM_CONCURRENT_STREAMS): + future = executor.submit(stream_worker, i, stats, prompts) + futures.append(future) + + # Wait for all streams to complete + for future in futures: + future.result() + + except KeyboardInterrupt: + print("\n\nāš ļø All streams interrupted by user") + stats.is_streaming = False + status_bar.stop() + return + except Exception as e: + print(f"\n\nāŒ Error during concurrent streaming: {e}") + stats.is_streaming = False + status_bar.stop() + return + + # Stop streaming and status bar + stats.is_streaming = False + status_bar.stop() + + # Show final results + print("\n" + "=" * 80) + print("āœ… All concurrent streams completed!") + print("šŸ“Š Final Aggregated Statistics:") + print(f" 🌊 Total streams: {NUM_CONCURRENT_STREAMS}") + print(f" ā±ļø Total time: {stats.get_elapsed_time():.2f} seconds") + print(f" šŸ”¤ Total tokens: {stats.tokens_produced}") + print(f" šŸ“ Total characters: {stats.characters_produced}") + print(f" šŸ“„ Total lines: {stats.lines_produced}") + print(f" ⚔ Aggregated tokens/second: {stats.get_tokens_per_second():.2f}") + print(f" šŸš€ Aggregated characters/second: {stats.get_chars_per_second():.2f}") + print( + f" šŸŽÆ Average per stream: {stats.get_tokens_per_second() / NUM_CONCURRENT_STREAMS:.2f} tok/s" + ) + print("=" * 80) + + +if __name__ == "__main__": + main() diff --git a/clients/nilai-py/examples/5-nildb-prompt-storage.py b/clients/nilai-py/examples/5-nildb-prompt-storage.py new file mode 100644 index 00000000..994041ff --- /dev/null +++ b/clients/nilai-py/examples/5-nildb-prompt-storage.py @@ -0,0 +1,41 @@ +## NOTE: DELEGATION TOKEN MODE DOES NOT WORK +## AS THIS IS RESERVED TO SUBSCRIPTION OWNERS + + +from nilai_py import Client +from config import API_KEY + + +def main(): + # Initialize the client in API key mode + # To obtain an API key, navigate to https://nilpay.vercel.app/ + # and create a new subscription. + # The API key will be displayed in the subscription details. + # The Client class automatically handles the NUC token creation and management. + ## For sandbox, use the following: + client = Client( + base_url="https://nilai-f910.nillion.network/nuc/v1/", api_key=API_KEY + ) + + # Make a request to the Nilai API + # response = client.chat.completions.create( + # model="google/gemma-3-27b-it", + # messages=[ + # {"role": "user", "content": "What is your name?"} + # ], + # ) + + # print(f"Response: {response.choices[0].message.content}") + # List prompts from Nildb + client.list_prompts_from_nildb() + + store_ids = client.store_prompt_to_nildb( + prompt="You are a very clever model that answers with cheese answers and always starting with the word cheese" + ) + print("Stored document IDs:", store_ids) + + client.list_prompts_from_nildb() + + +if __name__ == "__main__": + main() diff --git a/clients/nilai-py/examples/6-nildb-stored-prompt.py b/clients/nilai-py/examples/6-nildb-stored-prompt.py new file mode 100644 index 00000000..c173d748 --- /dev/null +++ b/clients/nilai-py/examples/6-nildb-stored-prompt.py @@ -0,0 +1,163 @@ +""" +Example 6: Using stored prompts from NilDB with delegation token flow + +This example demonstrates how to: +1. Load private keys and stored prompt data from files +2. Set up a delegation token chain between subscription owner and prompt data owner +3. Use a client with delegation tokens to access stored prompts + +Key components: +- Subscription owner server (creates delegation tokens for API access) +- Prompt data owner server (manages access to stored prompt documents) +- Client (makes requests using delegation tokens) +""" + +import json +from typing import Dict, Any + +from nilai_py import ( + Client, + DelegationTokenServer, + AuthType, + DelegationServerConfig, + PromptDocumentInfo, + DelegationTokenServerType, +) + +from config import API_KEY + + +class FileLoader: + """Utility class for loading configuration files.""" + + @staticmethod + def load_private_key(filename: str) -> str: + """Load private key from JSON file.""" + with open(filename, "r") as f: + key_data = json.load(f) + return key_data["key"] + + @staticmethod + def load_stored_prompt_data(filename: str) -> Dict[str, str]: + """Load stored prompt data including DID and document IDs.""" + with open(filename, "r") as f: + prompt_data = json.load(f) + return { + "did": prompt_data["did"], + "doc_id": prompt_data["document_ids"][0], # Use the first document ID + } + + +class DelegationServerManager: + """Manages delegation token servers for the stored prompt flow.""" + + def __init__( + self, + api_key: str, + ): + self.api_key = api_key + + def create_subscription_owner_server(self) -> DelegationTokenServer: + """Create server for the subscription owner (manages API access).""" + return DelegationTokenServer( + private_key=self.api_key, + config=DelegationServerConfig( + expiration_time=10 * 60 * 60, # 10 hours + token_max_uses=10, + ), + ) + + def create_prompt_data_owner_server( + self, private_key: str, prompt_data: Dict[str, str] + ) -> DelegationTokenServer: + """Create server for the prompt data owner (manages document access).""" + return DelegationTokenServer( + private_key=private_key, + config=DelegationServerConfig( + mode=DelegationTokenServerType.DELEGATION_ISSUER, + expiration_time=10, # 10 seconds + token_max_uses=1, + prompt_document=PromptDocumentInfo( + doc_id=prompt_data["doc_id"], owner_did=prompt_data["did"] + ), + ), + ) + + +class StoredPromptClient: + """Client for making requests using stored prompts with delegation tokens.""" + + def __init__( + self, + base_url: str = "https://nilai-f910.nillion.network/nuc/v1/", + ): + self.client = Client( + base_url=base_url, + auth_type=AuthType.DELEGATION_TOKEN, + ) + + def setup_delegation(self, delegation_server: DelegationTokenServer) -> None: + """Set up delegation token for the client.""" + delegation_request = self.client.get_delegation_request() + delegation_token = delegation_server.create_delegation_token(delegation_request) + self.client.update_delegation(delegation_token) + + def create_completion(self, model: str, messages: list) -> Any: + """Create a chat completion using the configured client.""" + return self.client.chat.completions.create( + model=model, + messages=messages, + ) + + +def setup_delegation_chain( + subscription_server: DelegationTokenServer, prompt_server: DelegationTokenServer +) -> None: + """Set up the delegation chain between servers.""" + prompt_request = prompt_server.get_delegation_request() + delegation_token = subscription_server.create_delegation_token(prompt_request) + prompt_server.update_delegation_token(delegation_token.delegation_token) + + +def main(): + """Main execution flow for stored prompt example.""" + + # Load configuration files + loader = FileLoader() + private_key = loader.load_private_key("keys/private_key_20250922_165315.json") + stored_prompt_data = loader.load_stored_prompt_data( + "stored_prompts/stored_prompts-9bb6bb19-54a8-4992-a85a-faac3ea98637.json" + ) + + # Initialize server manager + server_manager = DelegationServerManager(API_KEY) + + # Create delegation servers + subscription_owner_server = server_manager.create_subscription_owner_server() + prompt_data_owner_server = server_manager.create_prompt_data_owner_server( + private_key, stored_prompt_data + ) + + # Set up delegation chain + setup_delegation_chain(subscription_owner_server, prompt_data_owner_server) + + # Initialize client and set up delegation + stored_prompt_client = StoredPromptClient() + stored_prompt_client.setup_delegation(prompt_data_owner_server) + + # Make request using stored prompt + response = stored_prompt_client.create_completion( + model="openai/gpt-oss-20b", + messages=[ + {"role": "user", "content": "Hello! Can you help me with something?"} + ], + ) + + print( + "Your response, if using the previous stored prompt should have a cheese answer:" + ) + print(f"Response: {response.choices[0].message.content}") + + +if __name__ == "__main__": + main() diff --git a/clients/nilai-py/examples/7-web-search.py b/clients/nilai-py/examples/7-web-search.py new file mode 100644 index 00000000..f18a9810 --- /dev/null +++ b/clients/nilai-py/examples/7-web-search.py @@ -0,0 +1,34 @@ +from nilai_py import Client + +from config import API_KEY + + +def main(): + # Initialize the client in API key mode + # To obtain an API key, navigate to https://nilpay.vercel.app/ + # and create a new subscription. + # The API key will be displayed in the subscription details. + # The Client class automatically handles the NUC token creation and management. + ## For sandbox, use the following: + client = Client( + base_url="https://nilai-f910.nillion.network/nuc/v1/", + api_key=API_KEY, + ) + + # Make a request to the Nilai API + response = client.chat.completions.create( + model="openai/gpt-oss-20b", + messages=[ + { + "role": "user", + "content": "Can you look for the latest news about AI and summarize them?", + } + ], + extra_body={"web_search": True}, + ) + + print(f"Response: {response.choices[0].message.content}") + + +if __name__ == "__main__": + main() diff --git a/clients/nilai-py/examples/README.md b/clients/nilai-py/examples/README.md new file mode 100644 index 00000000..abd5bf77 --- /dev/null +++ b/clients/nilai-py/examples/README.md @@ -0,0 +1,60 @@ +# Nilai Python SDK Examples + +This directory contains example scripts demonstrating how to use the Nilai Python SDK. + +## Examples + +### 1. API Key Mode (`0-api_key_mode.py`) +Basic example showing how to use the SDK with an API key for authentication. + +### 2. Delegation Token Mode (`1-delegation_token_mode.py`) +Advanced example showing how to use delegation tokens for authentication, including server-side token generation. + +### 3. Streaming Mode (`2-streaming_mode.py`) +Basic streaming example showing how to receive real-time responses from the API. + +### 4. Advanced Streaming (`3-advanced_streaming.py`) +Advanced streaming example with error handling, progress tracking, and custom processing. + +## Configuration + +All examples use the `config.py` file to load the API key from environment variables. Make sure to: + +1. Create a `.env` file in the project root +2. Add your API key: `API_KEY=your_api_key_here` +3. Or set the environment variable directly: `export API_KEY=your_api_key_here` + +## Running Examples + +```bash +# Basic API key example +python examples/0-api_key_mode.py + +# Delegation token example +python examples/1-delegation_token_mode.py + +# Streaming example +python examples/2-streaming_mode.py + +# Advanced streaming example +python examples/3-advanced_streaming.py +``` + +## Streaming Features + +The streaming examples demonstrate: + +- **Real-time response processing**: Receive and display responses as they're generated +- **Progress tracking**: Monitor chunk count and response length +- **Error handling**: Graceful handling of interruptions and errors +- **Custom processing**: Word counting, line tracking, and other real-time analysis +- **Retry logic**: Automatic retry on failures + +## Authentication + +The SDK supports two authentication modes: + +1. **API Key Mode**: Direct authentication using your API key +2. **Delegation Token Mode**: Server-side token generation for enhanced security + +Both modes support streaming responses. diff --git a/clients/nilai-py/examples/config.py b/clients/nilai-py/examples/config.py new file mode 100644 index 00000000..23bf9e71 --- /dev/null +++ b/clients/nilai-py/examples/config.py @@ -0,0 +1,14 @@ +import os +from dotenv import load_dotenv + +load_dotenv(override=True) + + +def get_api_key() -> str: + api_key: str | None = os.getenv("API_KEY", None) + if api_key is None: + raise ValueError("API_KEY is not set") + return api_key + + +API_KEY: str = get_api_key() diff --git a/clients/nilai-py/pyproject.toml b/clients/nilai-py/pyproject.toml new file mode 100644 index 00000000..d803a95c --- /dev/null +++ b/clients/nilai-py/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "nilai-py" +version = "0.0.0a0" +description = "Nilai Python SDK" +readme = "README.md" +authors = [ + { name = "JosĆ© Cabrero-Holgueras", email = "jose.cabrero@nillion.com" } +] +requires-python = ">=3.12" +dependencies = [ + "httpx>=0.28.1", + "nuc>=0.1.0", + "openai>=1.108.1", + "pydantic>=2.11.9", + "python-dotenv>=1.1.1", + "secretvaults>=0.2.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.metadata] +allow-direct-references = true + +[dependency-groups] +dev = [ + "pytest>=8.4.2", + "pytest-cov>=7.0.0", + "ruff>=0.13.1", +] diff --git a/clients/nilai-py/src/nilai_py/__init__.py b/clients/nilai-py/src/nilai_py/__init__.py new file mode 100644 index 00000000..49a220ba --- /dev/null +++ b/clients/nilai-py/src/nilai_py/__init__.py @@ -0,0 +1,21 @@ +from nilai_py.client import Client +from nilai_py.server import DelegationTokenServer +from nilai_py.niltypes import ( + DelegationTokenRequest, + DelegationTokenResponse, + AuthType, + DelegationServerConfig, + PromptDocumentInfo, + DelegationTokenServerType, +) + +__all__ = [ + "Client", + "DelegationTokenServer", + "DelegationTokenRequest", + "DelegationTokenResponse", + "AuthType", + "DelegationServerConfig", + "PromptDocumentInfo", + "DelegationTokenServerType", +] diff --git a/clients/nilai-py/src/nilai_py/client.py b/clients/nilai-py/src/nilai_py/client.py new file mode 100644 index 00000000..5b93f10a --- /dev/null +++ b/clients/nilai-py/src/nilai_py/client.py @@ -0,0 +1,237 @@ +import json +import os +import openai +from typing_extensions import override +from typing import List, Optional + + +import base64 +import httpx +import asyncio +import datetime + + +from nuc.envelope import NucTokenEnvelope +from nuc.token import Did, InvocationBody +from nuc.builder import NucTokenBuilder +from nilai_py.nildb import NilDBPromptManager + +from nilai_py.niltypes import ( + DelegationTokenRequest, + DelegationTokenResponse, + NilAuthPrivateKey, + NilAuthPublicKey, + AuthType, +) + +from nilai_py.common import is_expired, new_root_token + + +class Client(openai.Client): + def __init__(self, *args, **kwargs): + self.auth_type: AuthType = kwargs.pop("auth_type", AuthType.API_KEY) + + match self.auth_type: + case AuthType.API_KEY: + self._api_key_init(*args, **kwargs) + case AuthType.DELEGATION_TOKEN: + self._delegation_token_init(*args, **kwargs) + kwargs["api_key"] = ( + "" # This is a placeholder to avoid the api key being used in the super call + ) + + # Remove the nilauth_url from the kwargs + super().__init__(*args, **kwargs) + + # Retrieve the public key from the nilai server + try: + self.nilai_public_key = self._get_nilai_public_key() + print( + "Retrieved nilai public key:", self.nilai_public_key.serialize().hex() + ) + except Exception as e: + print(f"Failed to retrieve the nilai public key: {e}") + raise e + + def _api_key_init(self, *args, **kwargs): + # Initialize the nilauth private key with the subscription + self.api_key: str | None = kwargs.get("api_key", None) # pyright: ignore[reportIncompatibleVariableOverride] + if self.api_key is None: + raise ValueError("In API key mode, api_key is required") + + self.nilauth_private_key: NilAuthPrivateKey = NilAuthPrivateKey( # pyright: ignore[reportRedeclaration] + bytes.fromhex(self.api_key) + ) + # Initialize the root token envelope + self._root_token_envelope: Optional[NucTokenEnvelope] = None + + def _delegation_token_init(self, *args, **kwargs): + # Generate a new private key for the client + api_key = kwargs.get("api_key", None) + if api_key is not None: + self.nilauth_private_key = NilAuthPrivateKey(bytes.fromhex(api_key)) + else: + self.nilauth_private_key: NilAuthPrivateKey = NilAuthPrivateKey() + + @property + def root_token(self) -> NucTokenEnvelope: + """ + Get the root token envelope. If the root token is expired, it will be refreshed. + The root token is used to create delegation tokens. + + Returns: + NucTokenEnvelope: The root token envelope. + """ + if self.auth_type != AuthType.API_KEY: + raise RuntimeError("Root token is only available in API key mode") + + if self._root_token_envelope is None or is_expired(self._root_token_envelope): + self._root_token_envelope = new_root_token(self.nilauth_private_key) + + return self._root_token_envelope + + def _get_nilai_public_key(self) -> NilAuthPublicKey: + """ + Retrieve the nilai public key from the nilai server. + + Returns: + NilAuthPublicKey: The nilai public key. + + Raises: + RuntimeError: If the nilai public key cannot be retrieved. + """ + try: + public_key_response = httpx.get(f"{self.base_url}public_key", verify=False) + if public_key_response.status_code != 200: + raise RuntimeError( + f"Failed to retrieve the nilai public key: {public_key_response.text}" + ) + return NilAuthPublicKey( + base64.b64decode(public_key_response.text), raw=True + ) + except Exception as e: + raise RuntimeError(f"Failed to retrieve the nilai public key: {e}") + + def get_delegation_request(self) -> DelegationTokenRequest: + """ + Get the delegation request for the client. + + Returns: + DelegationTokenRequest: The delegation request. + """ + if self.nilauth_private_key.pubkey is None: + raise ValueError("Public key is None") + + delegation_request: DelegationTokenRequest = DelegationTokenRequest( + public_key=self.nilauth_private_key.pubkey.serialize().hex() + ) + return delegation_request + + def update_delegation(self, delegation_token_response: DelegationTokenResponse): + """ + Update the delegation token for the client. + """ + self.delegation_token = NucTokenEnvelope.parse( + delegation_token_response.delegation_token + ) + + def _get_invocation_token(self) -> str: + """ + Get the invocation token for the client. + + Returns: + str: The invocation token. + """ + match self.auth_type: + case AuthType.API_KEY: + return self._get_invocation_token_with_api_key() + case AuthType.DELEGATION_TOKEN: + return self._get_invocation_token_with_delegation() + case _: + raise RuntimeError("Invalid auth type") + + def _get_invocation_token_with_delegation(self) -> str: + """ + Get the invocation token for the client with delegation. + """ + if self.auth_type != AuthType.DELEGATION_TOKEN: + raise RuntimeError( + "Invocation token is only available through API key mode only" + ) + + invocation_token: str = ( + NucTokenBuilder.extending(self.delegation_token) + .body(InvocationBody(args={})) + .audience(Did(self.nilai_public_key.serialize())) + .build(self.nilauth_private_key) + ) + return invocation_token + + def _get_invocation_token_with_api_key(self) -> str: + """ + Get the invocation token for the client with API key. + """ + if self.auth_type != AuthType.API_KEY: + raise RuntimeError( + "Invocation token is only available through Delegation Token mode only" + ) + + invocation_token: str = ( + NucTokenBuilder.extending(self.root_token) + .body(InvocationBody(args={})) + .audience(Did(self.nilai_public_key.serialize())) + .build(self.nilauth_private_key) + ) + return invocation_token + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self._get_invocation_token() + return {"Authorization": f"Bearer {api_key}"} + + async def async_list_prompts_from_nildb(self) -> None: + prompt_manager = await NilDBPromptManager.init(nilai_url=str(self.base_url)) + await prompt_manager.list_prompts() + await prompt_manager.close() + + def list_prompts_from_nildb(self) -> None: + return asyncio.run(self.async_list_prompts_from_nildb()) + + async def async_store_prompt_to_nildb(self, prompt: str, dir: str) -> List[str]: + prompt_manager = await NilDBPromptManager.init(nilai_url=str(self.base_url)) + + invocation_token = self._get_invocation_token() + result = await prompt_manager.create_prompt( + prompt=prompt, nilai_invocation_token=invocation_token + ) + + await prompt_manager.close() + + # Extract document IDs from the result for storage + document_ids = [] + if result and hasattr(result, "root"): + for node_name, response in result.root.items(): # pyright: ignore[reportAttributeAccessIssue] + if hasattr(response, "data") and hasattr(response.data, "created"): + document_ids.extend(response.data.created) + + # Store the created document IDs to a json file + did = prompt_manager.user_result.keypair + if did is None: + raise ValueError("DID is None") + did = did.to_did_string() + + os.makedirs(dir, exist_ok=True) + storage_data = { + "prompt": prompt, + "created_at": datetime.datetime.now().isoformat(), + "did": did, + "document_ids": document_ids, + } + with open(f"{dir}/stored_prompts-{document_ids[0]}.json", "w+") as f: + json.dump(storage_data, f, indent=4) + + return document_ids + + def store_prompt_to_nildb(self, prompt: str, dir="./stored_prompts") -> List[str]: + return asyncio.run(self.async_store_prompt_to_nildb(prompt, dir=dir)) diff --git a/clients/nilai-py/src/nilai_py/common.py b/clients/nilai-py/src/nilai_py/common.py new file mode 100644 index 00000000..9b3270a4 --- /dev/null +++ b/clients/nilai-py/src/nilai_py/common.py @@ -0,0 +1,40 @@ +import datetime +from nuc.envelope import NucTokenEnvelope +from nuc.token import Command, NucToken, Did +from nuc.builder import NucTokenBuilder, DelegationBody +from secp256k1 import PrivateKey + + +def is_expired(token_envelope: NucTokenEnvelope) -> bool: + """ + Check if a token envelope is expired. + + Args: + token_envelope (NucTokenEnvelope): The token envelope to check. + + Returns: + bool: True if the token envelope is expired, False otherwise. + """ + token: NucToken = token_envelope.token.token + if token.expires_at is None: + return False + return token.expires_at < datetime.datetime.now(datetime.timezone.utc) + + +def new_root_token(private_key: PrivateKey) -> NucTokenEnvelope: + """ + Force the creation of a new root token. + """ + hex_public_key = private_key.pubkey + if hex_public_key is None: + raise ValueError("Public key is None") + hex_public_key = hex_public_key.serialize() + root_token = NucTokenBuilder( + body=DelegationBody([]), + audience=Did(hex_public_key), + subject=Did(hex_public_key), + expires_at=datetime.datetime.now(datetime.timezone.utc) + + datetime.timedelta(hours=1), + command=Command(["nil", "ai", "generate"]), + ).build(private_key) + return NucTokenEnvelope.parse(root_token) diff --git a/clients/nilai-py/src/nilai_py/nildb/__init__.py b/clients/nilai-py/src/nilai_py/nildb/__init__.py new file mode 100644 index 00000000..81dc64f2 --- /dev/null +++ b/clients/nilai-py/src/nilai_py/nildb/__init__.py @@ -0,0 +1,157 @@ +from typing import List, Optional +import httpx + +from secretvaults import SecretVaultUserClient +import uuid + +from nilai_py.nildb.models import ( + DocumentReference, + UserSetupResult, + PromptDelegationToken, +) +from nilai_py.nildb.config import NilDBConfig, DefaultNilDBConfig +from nilai_py.nildb.user import create_user_if_not_exists +from nilai_py.nildb.document import ( + create_document_core, + list_data_references_core, +) + + +class NilDBPromptManager(object): + """Manager for handling document prompts in NilDB""" + + def __init__(self, nilai_url: str, nildb_config: NilDBConfig = DefaultNilDBConfig): + self.nilai_url = nilai_url + self.nildb_config = nildb_config + self._client: Optional[SecretVaultUserClient] = None + self._user_result: Optional[UserSetupResult] = None + + @staticmethod + async def init( + nilai_url: str, nildb_config: NilDBConfig = DefaultNilDBConfig + ) -> "NilDBPromptManager": + """Async initializer to setup user and client""" + instance = NilDBPromptManager(nilai_url, nildb_config) + instance._user_result = await instance.setup_user() + instance._client = instance.user_result.user_client + return instance + + @property + def client(self) -> SecretVaultUserClient: + if not self._client: + raise RuntimeError("Client not initialized. Call setup_user() first.") + return self._client + + @property + def user_result(self) -> UserSetupResult: + if not self._user_result: + raise RuntimeError("User not initialized. Call setup_user() first.") + return self._user_result + + async def setup_user(self, keys_dir: str = "keys") -> UserSetupResult: + """Setup user keypair and client with configuration validation and error handling""" + result = await create_user_if_not_exists( + config=self.nildb_config, keys_dir=keys_dir + ) + + if not result.success: + raise RuntimeError(f"User setup failed: {result.error}") + else: + if result.keypair is not None: + print( + f"šŸŽ‰ User setup successful! šŸŽ‰\n šŸ”‘ Keys saved to: {result.keys_saved_to}\n šŸ” Public Key: {result.keypair.public_key_hex(compressed=True)}\n šŸ†” DID: {result.keypair.to_did_string()}" + ) + return result + + async def request_nildb_delegation_token(self, token=None) -> PromptDelegationToken: + # Use provided token, or fall back to env variable, or use default + + if self.user_result.keypair is None: + raise RuntimeError("User keypair is not initialized") + + prompt_delegation_token = httpx.get( + f"{self.nilai_url}delegation", + params={ + "prompt_delegation_request": self.user_result.keypair.to_did_string() + }, + verify=False, + headers={"Authorization": f"Bearer {token}"}, + ) + + print( + f"Delegation token response status: {prompt_delegation_token.status_code}" + ) + + if prompt_delegation_token.status_code != 200: + raise RuntimeError( + f"Failed to retrieve the delegation token: {prompt_delegation_token.text}" + ) + + return PromptDelegationToken(**prompt_delegation_token.json()) + + async def list_prompts(self) -> None: + """List all document references for the user""" + try: + result = await list_data_references_core(user_client=self.client) + + print( + "\n=== List Document References ===" + "\nListing all document references owned by the user:" + "\n" + "=" * 60 + ) + if result.success and result.data: + print("Document References:") + for ref in result.data: + print(f" - Collection: {ref.collection}, Document: {ref.document}") + else: + print("No document references found.") + except Exception as e: + print(f"An error occurred while listing document references: {str(e)}") + + async def create_prompt( + self, prompt: str, nilai_invocation_token: str + ) -> List[DocumentReference]: + """Store a new document prompt with the given content based on the document ID""" + print( + f"\n=== Create Document on {self.nildb_config.collection} for prompt: {prompt} ===" + ) + + try: + print( + f"\nšŸ“ Creating document in collection {self.nildb_config.collection}" + ) + print("=" * 60) + + # Load delegation token from file + print("šŸ”‘ Loading delegation token...") + delegation_token = await self.request_nildb_delegation_token( + token=nilai_invocation_token + ) + + # Fixed sample document data + + id = str(uuid.uuid4()) + data = {"_id": id, "prompt": {"%allot": prompt}} + print(f"šŸ“ Using document data: {data}") + + result = await create_document_core( + self.client, + self.nildb_config.collection, + data, + delegation_token.token, + delegation_token.did, + ) + if result.success: + print(f"āœ… Document created successfully! Document ID: {id}") + else: + print(f"āŒ Failed to create document: {result.error or result.message}") + return result.data if result.success and result.data else [] + except Exception as e: + print(f"An error occurred while creating the document: {str(e)}") + return [] + + async def close(self): + """Close the underlying client connection""" + if self._client: + await self._client.close() + self._client = None diff --git a/clients/nilai-py/src/nilai_py/nildb/config.py b/clients/nilai-py/src/nilai_py/nildb/config.py new file mode 100644 index 00000000..e68bc50f --- /dev/null +++ b/clients/nilai-py/src/nilai_py/nildb/config.py @@ -0,0 +1,55 @@ +import os +import enum + +from dotenv import load_dotenv +from typing import List +from pydantic import BaseModel, Field, field_validator +from secretvaults.common.types import Uuid + + +class NilDBConfig(BaseModel): + nilchain_url: str = Field(..., description="The URL of the Nilchain") + nilauth_url: str = Field(..., description="The URL of the Nilauth") + nodes: List[str] = Field(..., description="The URLs of the Nildb nodes") + collection: Uuid = Field(..., description="The ID of the collection") + + @field_validator("nodes", mode="before") + @classmethod + def parse_nodes(cls, v): + if isinstance(v, str): + return v.split(",") + return v + + @field_validator("collection", mode="before") + @classmethod + def parse_collection(cls, v): + if isinstance(v, str): + return Uuid(v) + return v + + +class NilDBCollection(enum.Enum): + SANDBOX = "e035f44e-9fb4-4560-b707-b9325c11207c" + PRODUCTION = "e035f44e-9fb4-4560-b707-b9325c11207c" + + +load_dotenv() + +# Initialize configuration from environment variables or defaults +DefaultNilDBConfig = NilDBConfig( + nilchain_url=os.getenv( + "NILDB_NILCHAIN_URL", "http://rpc.testnet.nilchain-rpc-proxy.nilogy.xyz" + ), + nilauth_url=os.getenv( + "NILDB_NILAUTH_URL", "https://nilauth.sandbox.app-cluster.sandbox.nilogy.xyz" + ), + nodes=os.getenv( + "NILDB_NODES", + "https://nildb-stg-n1.nillion.network,https://nildb-stg-n2.nillion.network,https://nildb-stg-n3.nillion.network", + ).split(","), + collection=Uuid(os.getenv("NILDB_COLLECTION", NilDBCollection.SANDBOX.value)), +) + +print( + f"Using NilDB Configuration:\n Nilchain URL: {DefaultNilDBConfig.nilchain_url}\n Nilauth URL: {DefaultNilDBConfig.nilauth_url}\n Nodes: {DefaultNilDBConfig.nodes}\n Collection ID: {DefaultNilDBConfig.collection}" +) diff --git a/clients/nilai-py/src/nilai_py/nildb/document.py b/clients/nilai-py/src/nilai_py/nildb/document.py new file mode 100644 index 00000000..daccaeec --- /dev/null +++ b/clients/nilai-py/src/nilai_py/nildb/document.py @@ -0,0 +1,194 @@ +from typing import Optional, Dict, Any + +from secretvaults import SecretVaultUserClient +from secretvaults.dto.users import ( + AclDto, + UpdateUserDataRequest, + ReadDataRequestParams, + DeleteDocumentRequestParams, +) +from secretvaults.dto.data import CreateOwnedDataRequest +from secretvaults.common.types import Uuid, Did + +from nilai_py.nildb.models import OperationResult + + +async def list_data_references_core( + user_client: SecretVaultUserClient, +) -> OperationResult: + """List all data references owned by the user - core functionality""" + try: + references_response = await user_client.list_data_references() + if not references_response: + return OperationResult( + success=False, message="No data references available" + ) + + if not hasattr(references_response, "data") or not references_response.data: + return OperationResult(success=False, message="No data references found") + + return OperationResult(success=True, data=references_response.data) + + except Exception: + return OperationResult(success=False, message="No data references available") + + +async def read_document_core( + user_client: SecretVaultUserClient, + collection_id: str, + document_id: str, + relevant_user: Optional[str] = None, +) -> OperationResult: + """Read a specific document - core functionality""" + try: + read_params = ReadDataRequestParams( + collection=Uuid(collection_id), + document=Uuid(document_id), + subject=Uuid(relevant_user) if relevant_user else None, + ) + document_response = await user_client.read_data(read_params) + if not document_response: + return OperationResult(success=False, message="No document data available") + + # Check if response has data attribute (wrapped response) + if hasattr(document_response, "data") and document_response.data: + document_data = document_response.data + else: + document_data = document_response + + if not document_data: + return OperationResult(success=False, message="No document data found") + + return OperationResult(success=True, data=document_data) + + except Exception as e: + return OperationResult(success=False, error=e) + + +async def delete_document_core( + user_client: SecretVaultUserClient, collection_id: str, document_id: str +) -> OperationResult: + """Delete a specific document - core functionality""" + try: + delete_params = DeleteDocumentRequestParams( + collection=Uuid(collection_id), document=Uuid(document_id) + ) + delete_response = await user_client.delete_data(delete_params) + + if delete_response: + node_count = ( + len(delete_response) if hasattr(delete_response, "__len__") else 1 + ) + return OperationResult( + success=True, + data=delete_response, + message=f"Deleted from {node_count} node(s)", + ) + else: + return OperationResult( + success=False, message="No response from delete operation" + ) + + except Exception as e: + return OperationResult(success=False, error=e) + + +async def update_document_core( + user_client: SecretVaultUserClient, + collection_id: str, + document_id: str, + update_data: Dict, +) -> OperationResult: + """Update a specific document - core functionality""" + try: + update_request = UpdateUserDataRequest( + collection=Uuid(collection_id), + document=Uuid(document_id), + update=update_data, + ) + update_response = await user_client.update_data(update_request) + + if update_response and hasattr(update_response, "root"): + has_errors = False + for _, response in update_response.root.items(): # pyright: ignore[reportAttributeAccessIssue] + if hasattr(response, "status") and response.status != 204: + has_errors = True + break + + if has_errors: + return OperationResult( + success=False, message="Update failed on some nodes" + ) + else: + node_count = len(update_response.root) # pyright: ignore[reportAttributeAccessIssue] + return OperationResult( + success=True, + data=update_response, + message=f"Updated on {node_count} node(s)", + ) + else: + return OperationResult( + success=False, message="No response from update operation" + ) + + except Exception as e: + return OperationResult(success=False, error=e) + + +async def create_document_core( + user_client: SecretVaultUserClient, + collection_id: str, + data: Dict[str, Any], + delegation_token: str, + builder_did: str, +) -> OperationResult: + """Create a document in a collection - core functionality""" + try: + # Create delegation token + + create_data_request = CreateOwnedDataRequest( + collection=Uuid(collection_id), + owner=Did(user_client.id), + data=[data], + acl=AclDto(grantee=Did(builder_did), read=True, write=False, execute=True), + ) + + create_response = await user_client.create_data( + delegation=delegation_token, body=create_data_request + ) + + # Calculate totals + total_created = 0 + total_errors = 0 + + if hasattr(create_response, "root"): + for _, response in create_response.root.items(): # pyright: ignore[reportAttributeAccessIssue] + if hasattr(response, "data"): + created_count = ( + len(response.data.created) if response.data.created else 0 + ) + error_count = ( + len(response.data.errors) if response.data.errors else 0 + ) + total_created += created_count + total_errors += error_count + + if total_errors > 0: + return OperationResult( + success=False, + message=f"Created {total_created} documents but had {total_errors} errors", + ) + else: + node_count = len(create_response.root) # pyright: ignore[reportAttributeAccessIssue] + return OperationResult( + success=True, + data=create_response, + message=f"Created document in {total_created} instances across {node_count} node(s)", + ) + else: + return OperationResult( + success=False, message="No response from create operation" + ) + + except Exception as e: + return OperationResult(success=False, error=e) diff --git a/clients/nilai-py/src/nilai_py/nildb/models.py b/clients/nilai-py/src/nilai_py/nildb/models.py new file mode 100644 index 00000000..62b3adf2 --- /dev/null +++ b/clients/nilai-py/src/nilai_py/nildb/models.py @@ -0,0 +1,154 @@ +""" +Common Pydantic models for nildb_wrapper package. + +This module provides base models and common types used across all modules. +""" + +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional, Any, Dict, Union +from enum import Enum +from datetime import datetime + +from secretvaults import SecretVaultUserClient +from secretvaults.common.keypair import Keypair + + +class BaseResult(BaseModel): + """Base result model for all operations""" + + model_config = ConfigDict( + extra="allow", + validate_assignment=True, + use_enum_values=True, + populate_by_name=True, + arbitrary_types_allowed=True, + ) + + success: bool + error: Optional[Union[str, Exception]] = None + message: Optional[str] = None + + +class PromptDelegationToken(BaseModel): + """Delegation token model""" + + model_config = ConfigDict(validate_assignment=True) + + token: str + did: str + + +class TimestampedModel(BaseModel): + """Base model with timestamp fields""" + + model_config = ConfigDict( + extra="allow", validate_assignment=True, populate_by_name=True + ) + + created_at: Optional[datetime] = Field(default_factory=datetime.now) + updated_at: Optional[datetime] = None + + +class KeyData(TimestampedModel): + """Model for key data in JSON files""" + + type: str + key: str + name: Optional[str] = None + + # For public keys + did: Optional[str] = None + private_key_file: Optional[str] = None + + # For private keys + public_key_file: Optional[str] = None + + +class KeypairInfo(BaseModel): + """Information about stored keypairs""" + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + private_key_file: str + public_key_file: Optional[str] = None + created_at: Optional[str] = None + name: str = "unnamed" + did: str = "unknown" + + +# User module models +class UserSetupResult(BaseResult): + """Result of user setup operation""" + + user_client: Optional[SecretVaultUserClient] = None + keypair: Optional[Keypair] = None + keys_saved_to: Optional[Dict[str, str]] = None + + +# Collection module models +class CollectionResult(BaseResult): + """Result of collection operations""" + + data: Optional[Any] = None + + +class CollectionCreationResult(BaseResult): + """Result of collection creation""" + + collection_id: Optional[str] = None + collection_name: Optional[str] = None + collection_type: Optional[str] = None + + +# Document module models +class OperationResult(BaseResult): + """Result of document operations""" + + data: Optional[Any] = None + + +class DocumentReference(BaseModel): + """Reference to a document""" + + model_config = ConfigDict(validate_assignment=True) + + builder: str + collection: str + document: str + + +# Builder module models +class RegistrationStatus(str, Enum): + """Builder registration status""" + + SUCCESS = "success" + ALREADY_REGISTERED = "already_registered" + ERROR = "error" + + +class DelegationToken(BaseModel): + """Delegation token model""" + + model_config = ConfigDict(validate_assignment=True) + + token: str + did: str + + +class RegistrationResult(BaseResult): + """Result of builder registration""" + + status: RegistrationStatus + response: Optional[Any] = None + + +class TokenData(TimestampedModel): + """Delegation token data for JSON serialization""" + + type: str = "delegation_token" + expires_at: datetime + user_did: str + builder_did: str + token: str + usage: str = "Use this token for data creation operations" + valid_for_seconds: int = 60 diff --git a/clients/nilai-py/src/nilai_py/nildb/user.py b/clients/nilai-py/src/nilai_py/nildb/user.py new file mode 100644 index 00000000..8913c2f5 --- /dev/null +++ b/clients/nilai-py/src/nilai_py/nildb/user.py @@ -0,0 +1,345 @@ +import datetime +import json +import os +import glob +from typing import Optional, Tuple, List + +from secretvaults.common.blindfold import BlindfoldFactoryConfig, BlindfoldOperation +from secretvaults.common.keypair import Keypair +from secretvaults import SecretVaultUserClient + +from nilai_py.nildb.models import UserSetupResult, KeyData, KeypairInfo +from nilai_py.nildb.config import NilDBConfig + + +def save_keypair_to_json( + keypair: Keypair, keys_dir: str = "keys" +) -> Tuple[bool, Optional[str], Optional[str]]: + """Save keypair to separate JSON files for private and public keys""" + try: + # Create keys directory if it doesn't exist + os.makedirs(keys_dir, exist_ok=True) + + # Generate timestamp for unique filenames + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + # File paths + private_key_file = os.path.join(keys_dir, f"private_key_{timestamp}.json") + public_key_file = os.path.join(keys_dir, f"public_key_{timestamp}.json") + + # Private key data using Pydantic model + private_key_data = KeyData( + type="private_key", + key=keypair.private_key_hex(), + public_key_file=public_key_file, + ) + + # Public key data using Pydantic model + public_key_data = KeyData( + type="public_key", + key=keypair.public_key_hex(), + did=keypair.to_did_string(), + private_key_file=private_key_file, + ) + + # Save private key + with open(private_key_file, "w") as f: + json.dump( + private_key_data.model_dump(mode="json"), f, indent=2, default=str + ) + + # Save public key + with open(public_key_file, "w") as f: + json.dump(public_key_data.model_dump(mode="json"), f, indent=2, default=str) + + return True, private_key_file, public_key_file + + except Exception as e: + return False, None, str(e) + + +def load_keypair_from_json( + private_key_file: str, +) -> Tuple[bool, Optional[Keypair], Optional[str]]: + """Load keypair from private key JSON file""" + try: + if not os.path.exists(private_key_file): + return False, None, f"Private key file not found: {private_key_file}" + + with open(private_key_file, "r") as f: + data = json.load(f) + + # Parse using Pydantic model + private_key_data = KeyData(**data) + + if private_key_data.type != "private_key": + return False, None, "Invalid private key file format" + + # Recreate keypair from private key hex + if not private_key_data.key: + return False, None, "No private key found in file" + + private_key_hex = private_key_data.key + + keypair = Keypair.from_hex(private_key_hex) + return True, keypair, None + + except Exception as e: + return False, None, str(e) + + +async def setup_user_core( + config: NilDBConfig, keys_dir: str = "keys" +) -> UserSetupResult: + """Setup user keypair and client - core functionality without UI concerns""" + try: + # Generate a new user keypair + user_keypair = Keypair.generate() + + # Save keypair to JSON files + save_success, private_file, public_file = save_keypair_to_json( + user_keypair, keys_dir + ) + if not save_success: + return UserSetupResult( + success=False, error=f"Failed to save keypair: {public_file}" + ) + + # Create user client + user_client = await SecretVaultUserClient.from_options( + keypair=user_keypair, + base_urls=config.nodes, + blindfold=BlindfoldFactoryConfig( + operation=BlindfoldOperation.STORE, use_cluster_key=True + ), + ) + + if private_file is None or public_file is None: + return UserSetupResult( + success=False, + error=f"Failed to save keypair: {private_file} or {public_file}", + ) + + return UserSetupResult( + success=True, + user_client=user_client, + keypair=user_keypair, + keys_saved_to={"private_key": private_file, "public_key": public_file}, + ) + + except Exception as e: + return UserSetupResult(success=False, error=e) + + +def store_keypair( + keypair: Keypair, keys_dir: str = "keys", name_prefix: Optional[str] = None +) -> Tuple[bool, Optional[str], Optional[str]]: + """Store keypair to files with optional custom prefix""" + if name_prefix: + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + private_key_file = os.path.join( + keys_dir, f"{name_prefix}_private_key_{timestamp}.json" + ) + public_key_file = os.path.join( + keys_dir, f"{name_prefix}_public_key_{timestamp}.json" + ) + + try: + os.makedirs(keys_dir, exist_ok=True) + + # Private key data using Pydantic model + private_key_data = KeyData( + type="private_key", + name=name_prefix, + key=keypair.private_key_hex(), + public_key_file=public_key_file, + ) + + # Public key data using Pydantic model + public_key_data = KeyData( + type="public_key", + name=name_prefix, + key=keypair.public_key_hex(), + did=keypair.to_did_string(), + private_key_file=private_key_file, + ) + + # Save private key + with open(private_key_file, "w") as f: + json.dump( + private_key_data.model_dump(mode="json"), f, indent=2, default=str + ) + + # Save public key + with open(public_key_file, "w") as f: + json.dump( + public_key_data.model_dump(mode="json"), f, indent=2, default=str + ) + + return True, private_key_file, public_key_file + + except Exception as e: + return False, None, str(e) + else: + # Use existing function for default behavior + return save_keypair_to_json(keypair, keys_dir) + + +def load_keypair( + private_key_file: str, +) -> Tuple[bool, Optional[Keypair], Optional[str]]: + """Load keypair from private key file (alias for load_keypair_from_json)""" + return load_keypair_from_json(private_key_file) + + +def load_keypair_by_name( + name_prefix: str, keys_dir: str = "keys" +) -> Tuple[bool, Optional[Keypair], Optional[str]]: + """Load keypair by searching for files with given name prefix""" + try: + # Search for private key files with the given prefix + pattern = os.path.join(keys_dir, f"{name_prefix}_private_key_*.json") + matching_files = glob.glob(pattern) + + if not matching_files: + return ( + False, + None, + f"No private key files found with prefix '{name_prefix}' in {keys_dir}", + ) + + # Sort by modification time, get the most recent + matching_files.sort(key=os.path.getmtime, reverse=True) + latest_file = matching_files[0] + + return load_keypair_from_json(latest_file) + + except Exception as e: + return False, None, str(e) + + +def list_stored_keypairs(keys_dir: str = "keys") -> List[KeypairInfo]: + """List all stored keypairs in the directory""" + try: + if not os.path.exists(keys_dir): + return [] + + keypairs = [] + pattern = os.path.join(keys_dir, "private_key_*.json") + private_key_files = glob.glob(pattern) + + for private_key_file in private_key_files: + try: + with open(private_key_file, "r") as f: + data = json.load(f) + + # Parse using Pydantic model + private_key_data = KeyData(**data) + + if private_key_data.type == "private_key": + # Try to load the keypair to get DID + success, keypair, _ = load_keypair_from_json(private_key_file) + + keypair_info = KeypairInfo( + private_key_file=private_key_file, + public_key_file=private_key_data.public_key_file, + created_at=private_key_data.created_at.isoformat() + if private_key_data.created_at + else None, + name=private_key_data.name or "unnamed", + did=keypair.to_did_string() + if success and keypair + else "unknown", + ) + keypairs.append(keypair_info) + except Exception: + continue # Skip invalid files + + # Sort by creation time, newest first + keypairs.sort(key=lambda x: x.created_at or "", reverse=True) + return keypairs + + except Exception: + return [] + + +def delete_keypair_files(private_key_file: str) -> Tuple[bool, Optional[str]]: + """Delete both private and public key files""" + try: + if not os.path.exists(private_key_file): + return False, f"Private key file not found: {private_key_file}" + + # Read private key file to find public key file + with open(private_key_file, "r") as f: + data = json.load(f) + + # Parse using Pydantic model + private_key_data = KeyData(**data) + public_key_file = private_key_data.public_key_file + + # Delete private key file + os.remove(private_key_file) + + # Delete public key file if it exists + if public_key_file and os.path.exists(public_key_file): + os.remove(public_key_file) + + return True, None + + except Exception as e: + return False, str(e) + + +async def create_user_if_not_exists( + config: NilDBConfig, keys_dir: str = "keys" +) -> UserSetupResult: + """Create a user if no existing keypair exists in the keys directory""" + try: + # Check if any keypairs already exist + existing_keypairs = list_stored_keypairs(keys_dir) + + if existing_keypairs: + # Load the most recent keypair + latest_keypair_info = existing_keypairs[ + 0 + ] # Already sorted by creation time + success, keypair, error = load_keypair_from_json( + latest_keypair_info.private_key_file + ) + if not success or keypair is None: + return UserSetupResult( + success=False, error=f"Failed to load existing keypair: {error}" + ) + + # Create user client with existing keypair + user_client = await SecretVaultUserClient.from_options( + keypair=keypair, + base_urls=config.nodes, + blindfold=BlindfoldFactoryConfig( + operation=BlindfoldOperation.STORE, use_cluster_key=True + ), + ) + + if ( + not latest_keypair_info.private_key_file + or not latest_keypair_info.public_key_file + ): + return UserSetupResult( + success=False, error=f"Failed to load existing keypair: {error}" + ) + + return UserSetupResult( + success=True, + user_client=user_client, + keypair=keypair, + keys_saved_to={ + "private_key": latest_keypair_info.private_key_file, + "public_key": latest_keypair_info.public_key_file, + }, + ) + else: + # No existing keypairs, create new user + return await setup_user_core(config, keys_dir) + + except Exception as e: + return UserSetupResult(success=False, error=str(e)) diff --git a/clients/nilai-py/src/nilai_py/niltypes.py b/clients/nilai-py/src/nilai_py/niltypes.py new file mode 100644 index 00000000..ccd90659 --- /dev/null +++ b/clients/nilai-py/src/nilai_py/niltypes.py @@ -0,0 +1,57 @@ +import enum +from typing import Optional +from pydantic import BaseModel +from secp256k1 import PrivateKey as NilAuthPrivateKey, PublicKey as NilAuthPublicKey + + +class AuthType(enum.Enum): + API_KEY = "API_KEY" + DELEGATION_TOKEN = "DELEGATION_TOKEN" + + +class DelegationTokenServerType(enum.Enum): + SUBSCRIPTION_OWNER = "SUBSCRIPTION_OWNER" + DELEGATION_ISSUER = "DELEGATION_ISSUER" + + +class PromptDocumentInfo(BaseModel): + doc_id: str + owner_did: str + + +class DelegationServerConfig(BaseModel): + mode: DelegationTokenServerType = DelegationTokenServerType.SUBSCRIPTION_OWNER + expiration_time: Optional[int] = 60 + token_max_uses: Optional[int] = 1 + prompt_document: Optional[PromptDocumentInfo] = None + + +class RequestType(enum.Enum): + DELEGATION_TOKEN_REQUEST = "DELEGATION_TOKEN_REQUEST" + DELEGATION_TOKEN_RESPONSE = "DELEGATION_TOKEN_RESPONSE" + + +class DelegationTokenRequest(BaseModel): + type: RequestType = RequestType.DELEGATION_TOKEN_REQUEST + public_key: str + + +class DelegationTokenResponse(BaseModel): + type: RequestType = RequestType.DELEGATION_TOKEN_RESPONSE + delegation_token: str + + +DefaultDelegationTokenServerConfig = DelegationServerConfig( + expiration_time=60, + token_max_uses=1, +) + +__all__ = [ + "NilAuthPrivateKey", + "NilAuthPublicKey", + "PromptDocumentInfo", + "AuthType", + "DelegationTokenRequest", + "DelegationTokenResponse", + "DefaultDelegationTokenServerConfig", +] diff --git a/nilai-attestation/src/nilai_attestation/py.typed b/clients/nilai-py/src/nilai_py/py.typed similarity index 100% rename from nilai-attestation/src/nilai_attestation/py.typed rename to clients/nilai-py/src/nilai_py/py.typed diff --git a/clients/nilai-py/src/nilai_py/server.py b/clients/nilai-py/src/nilai_py/server.py new file mode 100644 index 00000000..08f34846 --- /dev/null +++ b/clients/nilai-py/src/nilai_py/server.py @@ -0,0 +1,125 @@ +from typing import Any, Dict, Optional +from nilai_py.niltypes import ( + DelegationTokenRequest, + DelegationTokenResponse, + DelegationServerConfig, + DefaultDelegationTokenServerConfig, + DelegationTokenServerType, + NilAuthPrivateKey, +) + +from nilai_py.common import is_expired, new_root_token +from nuc.envelope import NucTokenEnvelope +from nuc.token import Did +from nuc.builder import NucTokenBuilder, Command +import datetime + + +class DelegationTokenServer: + def __init__( + self, + private_key: str, + config: DelegationServerConfig = DefaultDelegationTokenServerConfig, + ): + """ + Initialize the delegation token server. + + Args: + private_key (str): The private key of the server. + config (DelegationServerConfig): The configuration for the server. + """ + self.config: DelegationServerConfig = config + self.private_key: NilAuthPrivateKey = NilAuthPrivateKey( + bytes.fromhex(private_key) + ) + self._root_token_envelope: Optional[NucTokenEnvelope] = None + + @property + def root_token(self) -> Optional[NucTokenEnvelope]: + """ + Get the root token envelope. If the root token is expired, it will be refreshed. + The root token is used to create delegation tokens. + + Returns: + NucTokenEnvelope: The root token envelope. + """ + if self._root_token_envelope is None or is_expired(self._root_token_envelope): + if self.config.mode == DelegationTokenServerType.DELEGATION_ISSUER: + raise ValueError( + "In DELEGATION_ISSUER mode, the root token cannot be refreshed, it must be provided" + ) + self._root_token_envelope = new_root_token(self.private_key) + return self._root_token_envelope + + def update_delegation_token(self, root_token: str): + """ + Update the root token envelope. + + Args: + root_token (str): The new root token. + """ + if self.config.mode != DelegationTokenServerType.DELEGATION_ISSUER: + raise ValueError( + "Delegation token can only be updated in DELEGATION_ISSUER mode" + ) + self._root_token_envelope = NucTokenEnvelope.parse(root_token) + + def get_delegation_request(self) -> DelegationTokenRequest: + """ + Get the delegation request for the client. + + Returns: + DelegationTokenRequest: The delegation request. + """ + if self.private_key.pubkey is None: + raise ValueError("Public key is None") + delegation_request: DelegationTokenRequest = DelegationTokenRequest( + public_key=self.private_key.pubkey.serialize().hex() + ) + return delegation_request + + def create_delegation_token( + self, + delegation_token_request: DelegationTokenRequest, + config_override: Optional[DelegationServerConfig] = None, + ) -> DelegationTokenResponse: + """ + Create a delegation token. + + Args: + delegation_token_request (DelegationTokenRequest): The delegation token request. + config_override (DelegationServerConfig): The configuration override. + + Returns: + DelegationTokenResponse: The delegation token response. + """ + config: DelegationServerConfig = ( + config_override if config_override else self.config + ) + + public_key: bytes = bytes.fromhex(delegation_token_request.public_key) + + meta: Dict[str, Any] = { + "usage_limit": config.token_max_uses, + } + if config.prompt_document: + meta["document_id"] = config.prompt_document.doc_id + meta["document_owner_did"] = config.prompt_document.owner_did + + if self.root_token is None: + raise ValueError("Root token is None") + + delegated_token = ( + NucTokenBuilder.extending(self.root_token) + .expires_at( + datetime.datetime.now(datetime.timezone.utc) + + datetime.timedelta( + seconds=config.expiration_time if config.expiration_time else 10 + ) + ) + .audience(Did(public_key)) + .command(Command(["nil", "ai", "generate"])) + .meta(meta) + .build(self.private_key) + ) + return DelegationTokenResponse(delegation_token=delegated_token) diff --git a/clients/nilai-py/tests/e2e/__init__.py b/clients/nilai-py/tests/e2e/__init__.py new file mode 100644 index 00000000..9d5d4172 --- /dev/null +++ b/clients/nilai-py/tests/e2e/__init__.py @@ -0,0 +1,11 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + + +def get_api_key() -> str: + api_key = os.getenv("API_KEY") + if api_key is None: + raise ValueError("API_KEY is not set") + return api_key diff --git a/clients/nilai-py/tests/e2e/test_e2e.py b/clients/nilai-py/tests/e2e/test_e2e.py new file mode 100644 index 00000000..deb79827 --- /dev/null +++ b/clients/nilai-py/tests/e2e/test_e2e.py @@ -0,0 +1,138 @@ +import pytest +import openai +from nilai_py import ( + Client, + DelegationTokenServer, + AuthType, + DelegationServerConfig, + DelegationTokenRequest, + DelegationTokenResponse, +) + +from . import get_api_key + + +def test_e2e_api_key(): + client = Client( + base_url="https://nilai-a779.nillion.network/nuc/v1/", + api_key=get_api_key(), + ) + response = client.chat.completions.create( + model="meta-llama/Llama-3.2-3B-Instruct", + messages=[ + {"role": "user", "content": "Hello! Can you help me with something?"} + ], + ) + + print(f"Response: {response.choices[0].message.content}") + + +def test_e2e_delegation_token(): + server = DelegationTokenServer( + private_key=get_api_key(), + config=DelegationServerConfig( + expiration_time=10, # 1 second + token_max_uses=1, # 1 use + ), + ) + + client = Client( + base_url="https://nilai-a779.nillion.network/nuc/v1/", + auth_type=AuthType.DELEGATION_TOKEN, + ) + + # Client produces a delegation request + delegation_request: DelegationTokenRequest = client.get_delegation_request() + # Server creates a delegation token + delegation_token: DelegationTokenResponse = server.create_delegation_token( + delegation_request + ) + # Client updates the delegation token + client.update_delegation(delegation_token) + # Client uses the delegation token to make a request + response = client.chat.completions.create( + model="meta-llama/Llama-3.2-3B-Instruct", + messages=[ + {"role": "user", "content": "Hello! Can you help me with something?"} + ], + ) + + print(f"Response: {response.choices[0].message.content}") + + +def test_e2e_delegation_token_expired(): + server = DelegationTokenServer( + private_key=get_api_key(), + config=DelegationServerConfig( + expiration_time=0, # 0 seconds validity -> token is expired + token_max_uses=1, # 1 use + ), + ) + + client = Client( + base_url="https://nilai-a779.nillion.network/nuc/v1/", + auth_type=AuthType.DELEGATION_TOKEN, + ) + + # Client produces a delegation request + delegation_request: DelegationTokenRequest = client.get_delegation_request() + # Server creates a delegation token + delegation_token: DelegationTokenResponse = server.create_delegation_token( + delegation_request + ) + # Client updates the delegation token + client.update_delegation(delegation_token) + + with pytest.raises(openai.AuthenticationError): + # Client uses the delegation token to make a request + response = client.chat.completions.create( + model="meta-llama/Llama-3.2-3B-Instruct", + messages=[ + {"role": "user", "content": "Hello! Can you help me with something?"} + ], + ) + + print(f"Response: {response.choices[0].message.content}") + + +def test_e2e_delegation_token_max_uses(): + server = DelegationTokenServer( + private_key=get_api_key(), + config=DelegationServerConfig( + expiration_time=10, # 10 seconds validity -> token is not expired + token_max_uses=1, # 1 use -> token can be used once + ), + ) + + client = Client( + base_url="https://nilai-a779.nillion.network/nuc/v1/", + auth_type=AuthType.DELEGATION_TOKEN, + ) + + # Client produces a delegation request + delegation_request: DelegationTokenRequest = client.get_delegation_request() + # Server creates a delegation token + delegation_token: DelegationTokenResponse = server.create_delegation_token( + delegation_request + ) + # Client updates the delegation token + client.update_delegation(delegation_token) + # Client uses the delegation token to make a request + response = client.chat.completions.create( + model="meta-llama/Llama-3.2-3B-Instruct", + messages=[ + {"role": "user", "content": "Hello! Can you help me with something?"} + ], + ) + + print(f"Response: {response.choices[0].message.content}") + with pytest.raises(openai.RateLimitError): + # Client uses the delegation token to make a request + response = client.chat.completions.create( + model="meta-llama/Llama-3.2-3B-Instruct", + messages=[ + {"role": "user", "content": "Hello! Can you help me with something?"} + ], + ) + + print(f"Response: {response.choices[0].message.content}") diff --git a/clients/nilai-py/tests/unit/test_server.py b/clients/nilai-py/tests/unit/test_server.py new file mode 100644 index 00000000..e2524fe1 --- /dev/null +++ b/clients/nilai-py/tests/unit/test_server.py @@ -0,0 +1,355 @@ +""" +Comprehensive tests for the DelegationTokenServer class. + +This test suite provides 100% code coverage for the DelegationTokenServer class, +testing all public and private methods, error conditions, and edge cases. + +Test Coverage: +- Initialization with default and custom configurations +- Root token management (creation, caching, expiration handling) +- Token expiration checking logic +- Delegation token creation with various configurations +- Error handling for invalid keys and network failures +- Configuration property access +- Datetime calculations for token expiration + +The tests use extensive mocking to isolate the DelegationTokenServer logic +from external dependencies like NilauthClient and cryptographic operations. +""" + +import datetime +import pytest +from unittest.mock import Mock, patch +from nilai_py.server import DelegationTokenServer +from nilai_py.niltypes import ( + DelegationTokenRequest, + DelegationTokenResponse, + DelegationServerConfig, + DefaultDelegationTokenServerConfig, + RequestType, +) +from nilai_py.common import is_expired +from nuc.envelope import NucTokenEnvelope +from nuc.token import NucToken +from nuc.nilauth import BlindModule + + +class TestDelegationTokenServer: + """Test cases for DelegationTokenServer class.""" + + @pytest.fixture + def private_key_hex(self): + """Sample private key in hex format for testing.""" + return "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456" + + @pytest.fixture + def public_key_hex(self): + """Sample public key in hex format for testing.""" + return "04a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456" + + @pytest.fixture + def custom_config(self): + """Custom configuration for testing.""" + return DelegationServerConfig( + expiration_time=120, + token_max_uses=5, + ) + + @pytest.fixture + def delegation_request(self, public_key_hex): + """Sample delegation token request.""" + return DelegationTokenRequest(public_key=public_key_hex) + + @pytest.fixture + def mock_token_envelope(self): + """Mock token envelope for testing.""" + envelope = Mock(spec=NucTokenEnvelope) + token_wrapper = Mock() + token = Mock(spec=NucToken) + token.expires_at = datetime.datetime.now( + datetime.timezone.utc + ) + datetime.timedelta(hours=1) + token_wrapper.token = token + envelope.token = token_wrapper + return envelope + + @pytest.fixture + def expired_token_envelope(self): + """Mock expired token envelope for testing.""" + envelope = Mock(spec=NucTokenEnvelope) + token_wrapper = Mock() + token = Mock(spec=NucToken) + token.expires_at = datetime.datetime.now( + datetime.timezone.utc + ) - datetime.timedelta(hours=1) + token_wrapper.token = token + envelope.token = token_wrapper + return envelope + + def test_init_with_default_config(self, private_key_hex): + """Test server initialization with default configuration.""" + server = DelegationTokenServer(private_key_hex) + + assert server.config == DefaultDelegationTokenServerConfig + assert hasattr(server, "private_key") + + def test_init_with_custom_config(self, private_key_hex, custom_config): + """Test server initialization with custom configuration.""" + server = DelegationTokenServer( + private_key_hex, + ) + + assert server.config == custom_config + + def test_init_invalid_private_key(self): + """Test server initialization with invalid private key.""" + with pytest.raises(ValueError): + DelegationTokenServer("invalid_hex_key") + + def test_is_expired_with_expired_token( + self, private_key_hex, expired_token_envelope + ): + """Test _is_expired method with an expired token.""" + assert is_expired(expired_token_envelope) is True + + def test_is_expired_with_valid_token(self, private_key_hex, mock_token_envelope): + """Test _is_expired method with a valid token.""" + + assert is_expired(mock_token_envelope) is False + + def test_is_expired_with_no_expiration(self, private_key_hex): + """Test is_expired method with a token that has no expiration.""" + + envelope = Mock(spec=NucTokenEnvelope) + token_wrapper = Mock() + token = Mock(spec=NucToken) + token.expires_at = None + token_wrapper.token = token + envelope.token = token_wrapper + + assert is_expired(envelope) is False + + @patch("nilai_py.server.NilauthClient") + @patch("nilai_py.server.NucTokenEnvelope") + def test_root_token_first_access( + self, + mock_envelope_class, + mock_nilauth_client_class, + private_key_hex, + mock_token_envelope, + ): + """Test root_token property on first access.""" + # Setup mocks + mock_client = Mock() + mock_nilauth_client_class.return_value = mock_client + mock_client.request_token.return_value = "mock_token_response" + mock_envelope_class.parse.return_value = mock_token_envelope + + server = DelegationTokenServer(private_key_hex) + + # Access root_token property + result = server.root_token + + assert result == mock_token_envelope + mock_client.request_token.assert_called_once_with( + server.private_key, blind_module=BlindModule.NILAI + ) + mock_envelope_class.parse.assert_called_once_with("mock_token_response") + + @patch("nilai_py.server.NilauthClient") + @patch("nilai_py.server.NucTokenEnvelope") + def test_root_token_cached_access( + self, + mock_envelope_class, + mock_nilauth_client_class, + private_key_hex, + mock_token_envelope, + ): + """Test root_token property returns cached token when not expired.""" + # Setup mocks + mock_client = Mock() + mock_nilauth_client_class.return_value = mock_client + mock_client.request_token.return_value = "mock_token_response" + mock_envelope_class.parse.return_value = mock_token_envelope + + server = DelegationTokenServer(private_key_hex) + + # Access root_token property twice + result1 = server.root_token + result2 = server.root_token + + assert result1 == result2 == mock_token_envelope + # Should only call the client once (cached) + mock_client.request_token.assert_called_once() + + @patch("nilai_py.server.NilauthClient") + @patch("nilai_py.server.NucTokenEnvelope") + def test_root_token_refresh_when_expired( + self, + mock_envelope_class, + mock_nilauth_client_class, + private_key_hex, + expired_token_envelope, + mock_token_envelope, + ): + """Test root_token property refreshes when token is expired.""" + # Setup mocks + mock_client = Mock() + mock_nilauth_client_class.return_value = mock_client + mock_client.request_token.return_value = "mock_token_response" + mock_envelope_class.parse.side_effect = [ + expired_token_envelope, + mock_token_envelope, + ] + + server = DelegationTokenServer(private_key_hex) + + # Access root_token property twice + result1 = server.root_token # Should return expired token first + result2 = server.root_token # Should refresh and return new token + + assert result1 == expired_token_envelope + assert result2 == mock_token_envelope + # Should call the client twice (once for initial, once for refresh) + assert mock_client.request_token.call_count == 2 + + @patch("nilai_py.server.NucTokenBuilder") + def test_create_delegation_token_success( + self, + mock_builder_class, + private_key_hex, + delegation_request, + mock_token_envelope, + ): + """Test successful delegation token creation.""" + # Setup mocks + mock_builder = Mock() + mock_builder_class.extending.return_value = mock_builder + mock_builder.expires_at.return_value = mock_builder + mock_builder.audience.return_value = mock_builder + mock_builder.command.return_value = mock_builder + mock_builder.meta.return_value = mock_builder + mock_builder.build.return_value = "delegation_token_string" + + server = DelegationTokenServer(private_key_hex) + server._root_token_envelope = mock_token_envelope + + result = server.create_delegation_token(delegation_request) + + assert isinstance(result, DelegationTokenResponse) + assert result.delegation_token == "delegation_token_string" + assert result.type == RequestType.DELEGATION_TOKEN_RESPONSE + + # Verify builder chain calls + mock_builder_class.extending.assert_called_once_with(mock_token_envelope) + mock_builder.expires_at.assert_called_once() + mock_builder.audience.assert_called_once() + mock_builder.command.assert_called_once() + mock_builder.meta.assert_called_once() + mock_builder.build.assert_called_once_with(server.private_key) + + @patch("nilai_py.server.NucTokenBuilder") + def test_create_delegation_token_with_config_override( + self, + mock_builder_class, + private_key_hex, + delegation_request, + custom_config, + mock_token_envelope, + ): + """Test delegation token creation with configuration override.""" + # Setup mocks + mock_builder = Mock() + mock_builder_class.extending.return_value = mock_builder + mock_builder.expires_at.return_value = mock_builder + mock_builder.audience.return_value = mock_builder + mock_builder.command.return_value = mock_builder + mock_builder.meta.return_value = mock_builder + mock_builder.build.return_value = "delegation_token_string" + + server = DelegationTokenServer(private_key_hex) + server._root_token_envelope = mock_token_envelope + + result = server.create_delegation_token( + delegation_request, config_override=custom_config + ) + + assert isinstance(result, DelegationTokenResponse) + + # Verify that the custom config values are used + mock_builder.meta.assert_called_once_with( + {"usage_limit": custom_config.token_max_uses} + ) + + def test_create_delegation_token_invalid_public_key( + self, private_key_hex, mock_token_envelope + ): + """Test delegation token creation with invalid public key.""" + server = DelegationTokenServer(private_key_hex) + server._root_token_envelope = mock_token_envelope + + invalid_request = DelegationTokenRequest(public_key="invalid_hex") + + with pytest.raises(ValueError): + server.create_delegation_token(invalid_request) + + @patch("nilai_py.server.NilauthClient") + def test_nilauth_client_error_handling( + self, mock_nilauth_client_class, private_key_hex + ): + """Test error handling when NilauthClient fails.""" + # Setup mock to raise an exception + mock_client = Mock() + mock_nilauth_client_class.return_value = mock_client + mock_client.request_token.side_effect = Exception("Network error") + + server = DelegationTokenServer(private_key_hex) + + with pytest.raises(Exception, match="Network error"): + _ = server.root_token + + def test_config_properties_access(self, private_key_hex, custom_config): + """Test that configuration properties are properly accessible.""" + server = DelegationTokenServer(private_key_hex, config=custom_config) + + assert server.config.expiration_time == 120 + assert server.config.token_max_uses == 5 + + @patch("nilai_py.server.datetime") + def test_expiration_time_calculation( + self, + mock_datetime_module, + private_key_hex, + delegation_request, + mock_token_envelope, + ): + """Test that expiration time is calculated correctly.""" + # Setup datetime mock + fixed_now = datetime.datetime( + 2024, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc + ) + mock_datetime_module.datetime.now.return_value = fixed_now + mock_datetime_module.timedelta = datetime.timedelta + mock_datetime_module.timezone = datetime.timezone + + with ( + patch("nilai_py.server.NucTokenBuilder") as mock_builder_class, + ): + mock_builder = Mock() + mock_builder_class.extending.return_value = mock_builder + mock_builder.expires_at.return_value = mock_builder + mock_builder.audience.return_value = mock_builder + mock_builder.command.return_value = mock_builder + mock_builder.meta.return_value = mock_builder + mock_builder.build.return_value = "delegation_token_string" + + server = DelegationTokenServer(private_key_hex) + server._root_token_envelope = mock_token_envelope + + server.create_delegation_token(delegation_request) + + # Verify expires_at was called with correct expiration time + expected_expiration = fixed_now + datetime.timedelta( + seconds=60 + ) # Default expiration + mock_builder.expires_at.assert_called_once_with(expected_expiration) diff --git a/clients/nilai-py/uv.lock b/clients/nilai-py/uv.lock new file mode 100644 index 00000000..107f0037 --- /dev/null +++ b/clients/nilai-py/uv.lock @@ -0,0 +1,1272 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "bcl" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/78/57a3b26ac13312ed5901f1089f0351dfd958d19e96242d557e25c1498a95/bcl-2.3.1.tar.gz", hash = "sha256:2a10f1e4fde1c146594fe835f29c9c9753a9f1c449617578c1473d6371da9853", size = 16823, upload-time = "2022-10-04T01:56:50.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/93/f712cab57d0424ff65b380e22cb286b35b8bc0ba7997926dc18c8600f451/bcl-2.3.1-cp310-abi3-macosx_10_10_universal2.whl", hash = "sha256:cf59d66d4dd653b43b197ad5fc140a131db7f842c192d9836f5a6fe2bee9019e", size = 525696, upload-time = "2022-10-04T01:56:15.925Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a7/984bdb769c5ad2549fafc9365b0f6156fbeeec7df524eb064e65b164f8d0/bcl-2.3.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7696201b8111e877d21c1afd5a376f27975688658fa9001278f15e9fa3da2e0", size = 740158, upload-time = "2022-10-04T01:56:18.596Z" }, + { url = "https://files.pythonhosted.org/packages/36/e3/c860ae7aa62ddacf0ff4e1d2c9741f0d2ab65fec00e3890e8ac0f5463629/bcl-2.3.1-cp310-abi3-win32.whl", hash = "sha256:28f55e08e929309eacf09118b29ffb4d110ce3702eef18e98b8b413d0dfb1bf9", size = 88671, upload-time = "2022-10-04T01:56:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/a78ec72cfc2d6f438bd2978e81e05e708953434db8614a9f4f20bb7fa606/bcl-2.3.1-cp310-abi3-win_amd64.whl", hash = "sha256:f65e9f347b76964d91294964559da05cdcefb1f0bdfe90b6173892de3598a810", size = 96393, upload-time = "2022-10-04T01:56:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/25/f0/63337a824e34d0a3f48f2739d902c9c7d30524d4fc23ad73a3dcdad82e05/bcl-2.3.1-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:edb8277faee90121a248d26b308f4f007da1faedfd98d246841fb0f108e47db2", size = 315551, upload-time = "2022-10-04T01:56:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/00/1a/20ea61d352d5804df96baf8ca70401b17db8d748a81d4225f223f2580022/bcl-2.3.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99aff16e0da7a3b678c6cba9be24760eda75c068cba2b85604cf41818e2ba732", size = 740123, upload-time = "2022-10-04T01:56:26.995Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/2714e3f7d5643f487b0ecd49b21fa8db2d9572901baa49a6e0457a3b0c19/bcl-2.3.1-cp37-abi3-win32.whl", hash = "sha256:17d2e7dbe852c4447a7a2ff179dc466a3b8809ad1f151c4625ef7feff167fcaf", size = 88674, upload-time = "2022-10-04T01:56:28.518Z" }, + { url = "https://files.pythonhosted.org/packages/26/69/6fab32cd6888887ed9113b806854ac696a76cf77febdacc6c5d4271cba8e/bcl-2.3.1-cp37-abi3-win_amd64.whl", hash = "sha256:fb778e77653735ac0bd2376636cba27ad972e0888227d4b40f49ea7ca5bceefa", size = 96395, upload-time = "2022-10-04T01:56:29.948Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7a/06d9297f9805da15775615bb9229b38eb28f1e113cdd05d0e7bbcc3429e4/bcl-2.3.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f6d551e139fa1544f7c822be57b0a8da2dff791c7ffa152bf371e3a8712b8b62", size = 315576, upload-time = "2022-10-04T01:56:32.63Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/c244b97a2ffb839fc763cbd2ce65b9290c166e279aa9fc05f046e8feb372/bcl-2.3.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:447835deb112f75f89cca34e34957a36e355a102a37a7b41e83e5502b11fc10a", size = 740435, upload-time = "2022-10-04T01:56:35.392Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ff/25eaaf928078fc266d5f4cd485206acaec43c6a9311cf809114833bc24c4/bcl-2.3.1-cp38-abi3-win32.whl", hash = "sha256:1d8e0a25921ee705840219ed3c78e1d2e9d0d73cb2007c2708af57489bd6ce57", size = 88675, upload-time = "2022-10-04T01:56:36.943Z" }, + { url = "https://files.pythonhosted.org/packages/85/e3/a0e02b0da403503015c2196e812c8d3781ffcd94426ce5baf7f4bbfa8533/bcl-2.3.1-cp38-abi3-win_amd64.whl", hash = "sha256:a7312d21f5e8960b121fadbd950659bc58745282c1c2415e13150590d2bb271e", size = 96399, upload-time = "2022-10-04T01:56:38.555Z" }, + { url = "https://files.pythonhosted.org/packages/08/ad/a46220911bd7795f9aec10b195e1828b2e48c2015ef7e088447cba5e9089/bcl-2.3.1-cp39-abi3-macosx_10_10_universal2.whl", hash = "sha256:bb695832cb555bb0e3dee985871e6cfc2d5314fb69bbf62297f81ba645e99257", size = 525703, upload-time = "2022-10-04T01:56:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3a/e8395071a89a7199363990968d438b77c55d55cce556327c98d5ce7975d1/bcl-2.3.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0922349eb5ffd19418f46c40469d132c6e0aea0e47fec48a69bec5191ee56bec", size = 315583, upload-time = "2022-10-04T01:56:42.88Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/2be5d88275d3d7e79cdbc8d52659b02b752d44f2bf90addb987d1fb96752/bcl-2.3.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97117d57cf90679dd1b28f1039fa2090f5561d3c1ee4fe4e78d1b0680cc39b8d", size = 740137, upload-time = "2022-10-04T01:56:46.148Z" }, + { url = "https://files.pythonhosted.org/packages/7f/94/a3613caee8ca933902831343cc1040bcf3bb736cc9f38b2b4a7766292585/bcl-2.3.1-cp39-abi3-win32.whl", hash = "sha256:a5823f1b655a37259a06aa348bbc2e7a38d39d0e1683ea0596b888b7ef56d378", size = 88675, upload-time = "2022-10-04T01:56:47.459Z" }, + { url = "https://files.pythonhosted.org/packages/9e/45/302d6712a8ff733a259446a7d24ff3c868715103032f50eef0d93ba70221/bcl-2.3.1-cp39-abi3-win_amd64.whl", hash = "sha256:52cf26c4ecd76e806c6576c4848633ff44ebfff528fca63ad0e52085b6ba5aa9", size = 96394, upload-time = "2022-10-04T01:56:48.909Z" }, +] + +[[package]] +name = "bech32" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/fe/b67ac9b123e25a3c1b8fc3f3c92648804516ab44215adb165284e024c43f/bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899", size = 3695, upload-time = "2020-02-17T15:31:09.763Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/41/7022a226e5a6ac7091a95ba36bad057012ab7330b9894ad4e14e31d0b858/bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981", size = 4587, upload-time = "2020-02-17T15:31:08.299Z" }, +] + +[[package]] +name = "blindfold" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcl" }, + { name = "lagrange" }, + { name = "pailliers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/8d/89a66ce3f4d23f0b5c58c18fa207d51809d8a9489ef73a47d6c792edeb2b/blindfold-0.1.0.tar.gz", hash = "sha256:6e2c4bf315ea92abe0dbcb2034567194199bce0985f61ca96af5280310e7d116", size = 20400, upload-time = "2025-09-15T19:30:31.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/8b/92ee82fe4429cf4f87620e5f937ccdc88f6891512147d99edab683a06646/blindfold-0.1.0-py3-none-any.whl", hash = "sha256:71679c4e19569ec30aaf539974a5fb974b62baebdcb7a807eed996bfb22621b4", size = 14399, upload-time = "2025-09-15T19:30:30.366Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cosmpy" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bech32" }, + { name = "ecdsa" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "jsonschema" }, + { name = "protobuf" }, + { name = "pycryptodome" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/5f/39167cf97a03813911e518d1b615c4ef5fc3e4eb26454b8cb3b557a03fba/cosmpy-0.9.2.tar.gz", hash = "sha256:0f0eb80152f28ef5ee4d846d581d2e34ba2d952900f0e3570cacb84bb376f664", size = 205720, upload-time = "2024-01-11T13:07:04.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/bf/2b5e594858b0d41e372c9e4f975b3e5b2b655af1670f3a600d684d5c68d4/cosmpy-0.9.2-py3-none-any.whl", hash = "sha256:3591311198b08a0aa75340851ca166669974f17ffaa207a8d2cb26504fb0fa19", size = 413103, upload-time = "2024-01-11T13:07:02.595Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "ecdsa" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" }, +] + +[[package]] +name = "egcd" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/f5/c0c0808f8a3f8a4af605b48a241b16a634ceddd41b5e3ee05ae2fd9e1e42/egcd-2.0.2.tar.gz", hash = "sha256:3b05b0feb67549f8f76c97afed36c53252c0d7cb9a65bf4e6ca8b99110fb77f2", size = 6952, upload-time = "2024-12-31T21:05:21.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/e7/9d984faee490e50a495b50d0a87c42fe661252f9513157776d8cb2724445/egcd-2.0.2-py3-none-any.whl", hash = "sha256:2f0576a651b4aa9e9c4640bba078f9741d1624f386b55cb5363a79ae4b564bd2", size = 7187, upload-time = "2024-12-31T21:05:19.098Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, +] + +[[package]] +name = "grpcio" +version = "1.73.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/e8/b43b851537da2e2f03fa8be1aef207e5cbfb1a2e014fbb6b40d24c177cd3/grpcio-1.73.1.tar.gz", hash = "sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87", size = 12730355, upload-time = "2025-06-26T01:53:24.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/41/456caf570c55d5ac26f4c1f2db1f2ac1467d5bf3bcd660cba3e0a25b195f/grpcio-1.73.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:921b25618b084e75d424a9f8e6403bfeb7abef074bb6c3174701e0f2542debcf", size = 5334621, upload-time = "2025-06-26T01:52:23.602Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c2/9a15e179e49f235bb5e63b01590658c03747a43c9775e20c4e13ca04f4c4/grpcio-1.73.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:277b426a0ed341e8447fbf6c1d6b68c952adddf585ea4685aa563de0f03df887", size = 10601131, upload-time = "2025-06-26T01:52:25.691Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/1d39e90ef6348a0964caa7c5c4d05f3bae2c51ab429eb7d2e21198ac9b6d/grpcio-1.73.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:96c112333309493c10e118d92f04594f9055774757f5d101b39f8150f8c25582", size = 5759268, upload-time = "2025-06-26T01:52:27.631Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2b/2dfe9ae43de75616177bc576df4c36d6401e0959833b2e5b2d58d50c1f6b/grpcio-1.73.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f48e862aed925ae987eb7084409a80985de75243389dc9d9c271dd711e589918", size = 6409791, upload-time = "2025-06-26T01:52:29.711Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/e8fe779b23b5a26d1b6949e5c70bc0a5fd08f61a6ec5ac7760d589229511/grpcio-1.73.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a6c2cce218e28f5040429835fa34a29319071079e3169f9543c3fbeff166d2", size = 6003728, upload-time = "2025-06-26T01:52:31.352Z" }, + { url = "https://files.pythonhosted.org/packages/a9/39/57a18fcef567784108c4fc3f5441cb9938ae5a51378505aafe81e8e15ecc/grpcio-1.73.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:65b0458a10b100d815a8426b1442bd17001fdb77ea13665b2f7dc9e8587fdc6b", size = 6103364, upload-time = "2025-06-26T01:52:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/c5/46/28919d2aa038712fc399d02fa83e998abd8c1f46c2680c5689deca06d1b2/grpcio-1.73.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0a9f3ea8dce9eae9d7cb36827200133a72b37a63896e0e61a9d5ec7d61a59ab1", size = 6749194, upload-time = "2025-06-26T01:52:34.734Z" }, + { url = "https://files.pythonhosted.org/packages/3d/56/3898526f1fad588c5d19a29ea0a3a4996fb4fa7d7c02dc1be0c9fd188b62/grpcio-1.73.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:de18769aea47f18e782bf6819a37c1c528914bfd5683b8782b9da356506190c8", size = 6283902, upload-time = "2025-06-26T01:52:36.503Z" }, + { url = "https://files.pythonhosted.org/packages/dc/64/18b77b89c5870d8ea91818feb0c3ffb5b31b48d1b0ee3e0f0d539730fea3/grpcio-1.73.1-cp312-cp312-win32.whl", hash = "sha256:24e06a5319e33041e322d32c62b1e728f18ab8c9dbc91729a3d9f9e3ed336642", size = 3668687, upload-time = "2025-06-26T01:52:38.678Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/302448ca6e52f2a77166b2e2ed75f5d08feca4f2145faf75cb768cccb25b/grpcio-1.73.1-cp312-cp312-win_amd64.whl", hash = "sha256:303c8135d8ab176f8038c14cc10d698ae1db9c480f2b2823f7a987aa2a4c5646", size = 4334887, upload-time = "2025-06-26T01:52:40.743Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/4ca20d1acbefabcaba633ab17f4244cbbe8eca877df01517207bd6655914/grpcio-1.73.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b310824ab5092cf74750ebd8a8a8981c1810cb2b363210e70d06ef37ad80d4f9", size = 5335615, upload-time = "2025-06-26T01:52:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/75/ed/45c345f284abec5d4f6d77cbca9c52c39b554397eb7de7d2fcf440bcd049/grpcio-1.73.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8f5a6df3fba31a3485096ac85b2e34b9666ffb0590df0cd044f58694e6a1f6b5", size = 10595497, upload-time = "2025-06-26T01:52:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/a4/75/bff2c2728018f546d812b755455014bc718f8cdcbf5c84f1f6e5494443a8/grpcio-1.73.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:052e28fe9c41357da42250a91926a3e2f74c046575c070b69659467ca5aa976b", size = 5765321, upload-time = "2025-06-26T01:52:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/70/3b/14e43158d3b81a38251b1d231dfb45a9b492d872102a919fbf7ba4ac20cd/grpcio-1.73.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c0bf15f629b1497436596b1cbddddfa3234273490229ca29561209778ebe182", size = 6415436, upload-time = "2025-06-26T01:52:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3f/81d9650ca40b54338336fd360f36773be8cb6c07c036e751d8996eb96598/grpcio-1.73.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab860d5bfa788c5a021fba264802e2593688cd965d1374d31d2b1a34cacd854", size = 6007012, upload-time = "2025-06-26T01:52:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/59edf5af68d684d0f4f7ad9462a418ac517201c238551529098c9aa28cb0/grpcio-1.73.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ad1d958c31cc91ab050bd8a91355480b8e0683e21176522bacea225ce51163f2", size = 6105209, upload-time = "2025-06-26T01:52:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a8/700d034d5d0786a5ba14bfa9ce974ed4c976936c2748c2bd87aa50f69b36/grpcio-1.73.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f43ffb3bd415c57224c7427bfb9e6c46a0b6e998754bfa0d00f408e1873dcbb5", size = 6753655, upload-time = "2025-06-26T01:52:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/1f/29/efbd4ac837c23bc48e34bbaf32bd429f0dc9ad7f80721cdb4622144c118c/grpcio-1.73.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:686231cdd03a8a8055f798b2b54b19428cdf18fa1549bee92249b43607c42668", size = 6287288, upload-time = "2025-06-26T01:52:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/c6045d2ce16624bbe18b5d169c1a5ce4d6c3a47bc9d0e5c4fa6a50ed1239/grpcio-1.73.1-cp313-cp313-win32.whl", hash = "sha256:89018866a096e2ce21e05eabed1567479713ebe57b1db7cbb0f1e3b896793ba4", size = 3668151, upload-time = "2025-06-26T01:52:59.405Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/77ac689216daee10de318db5aa1b88d159432dc76a130948a56b3aa671a2/grpcio-1.73.1-cp313-cp313-win_amd64.whl", hash = "sha256:4a68f8c9966b94dff693670a5cf2b54888a48a5011c5d9ce2295a1a1465ee84f", size = 4335747, upload-time = "2025-06-26T01:53:01.233Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262, upload-time = "2025-05-18T19:03:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124, upload-time = "2025-05-18T19:03:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" }, + { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" }, + { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538, upload-time = "2025-05-18T19:03:55.046Z" }, + { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload-time = "2025-05-18T19:03:59.025Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload-time = "2025-05-18T19:04:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" }, + { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" }, + { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" }, + { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" }, + { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "lagrange" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/9d/4b6470fd6769b0943fbda9b30e2068bb8d9940be2977b1e80a184d527fa6/lagrange-3.0.1.tar.gz", hash = "sha256:272f352a676679ee318b0b302054f667f23afb73d10063cd3926c612527e09f1", size = 6894, upload-time = "2025-01-01T01:33:14.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d8/f1c3ff60a8b3e114cfb3e9eed75140d2a3e1e766791cfe2f210a5c736d61/lagrange-3.0.1-py3-none-any.whl", hash = "sha256:d473913d901f0c257456c505e4a94450f2e4a2f147460a68ad0cfb9ea33a6d0a", size = 6905, upload-time = "2025-01-01T01:33:11.031Z" }, +] + +[[package]] +name = "multidict" +version = "6.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, + { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" }, + { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" }, + { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" }, + { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, +] + +[[package]] +name = "nilai-py" +version = "0.0.0a0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "nuc" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "secretvaults" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "nuc", specifier = ">=0.1.0" }, + { name = "openai", specifier = ">=1.108.1" }, + { name = "pydantic", specifier = ">=2.11.9" }, + { name = "python-dotenv", specifier = ">=1.1.1" }, + { name = "secretvaults", specifier = ">=0.2.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "ruff", specifier = ">=0.13.1" }, +] + +[[package]] +name = "nuc" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cosmpy" }, + { name = "requests" }, + { name = "secp256k1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/58/acfdbdd6dc8e8575a1bc2ade9eedf7d33d99ac428573df5a46a4f4b76949/nuc-0.1.0.tar.gz", hash = "sha256:6a715bf07a8adf2901b68c9597ba44ae28506c3fb0fa03530c092bc0f8ba22f0", size = 29586, upload-time = "2025-07-01T14:46:55.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/ba/a99b12ee5132976d974fe65f9dbeaaafe4183a8558859c72bd271f87e25c/nuc-0.1.0-py3-none-any.whl", hash = "sha256:6845133866f2d41592be74ca2a41295d09d7a6d89886a5a1181dceefd4fe5a65", size = 22513, upload-time = "2025-07-01T14:46:54.685Z" }, +] + +[[package]] +name = "openai" +version = "1.108.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/7a/3f2fbdf82a22d48405c1872f7c3176a705eee80ff2d2715d29472089171f/openai-1.108.1.tar.gz", hash = "sha256:6648468c1aec4eacfa554001e933a9fa075f57bacfc27588c2e34456cee9fef9", size = 563735, upload-time = "2025-09-19T16:52:20.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/87/6ad18ce0e7b910e3706480451df48ff9e0af3b55e5db565adafd68a0706a/openai-1.108.1-py3-none-any.whl", hash = "sha256:952fc027e300b2ac23be92b064eac136a2bc58274cec16f5d2906c361340d59b", size = 948394, upload-time = "2025-09-19T16:52:18.369Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pailliers" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "egcd" }, + { name = "rabinmiller" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c2/578c08af348247c025179e9f22d4970549fd58635d3881a9ac86192b159b/pailliers-0.2.0.tar.gz", hash = "sha256:a1d3d7d840594f51073e531078b3da4dc5a7a527b410102a0f0fa65d6c222871", size = 8919, upload-time = "2025-01-01T23:18:57.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/0e/d793836d158ea15f7705e8ae705d73991f58e3eda0dde07e64bc423a4c12/pailliers-0.2.0-py3-none-any.whl", hash = "sha256:ad0ddc72be63f9b3c10200e23178fe527b566c4aa86659ab54a8faeb367ac7d6", size = 7404, upload-time = "2025-01-01T23:18:54.718Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + +[[package]] +name = "protobuf" +version = "4.25.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/01/34c8d2b6354906d728703cb9d546a0e534de479e25f1b581e4094c4a85cc/protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", size = 380920, upload-time = "2025-05-28T14:22:25.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ff/05f34305fe6b85bbfbecbc559d423a5985605cad5eda4f47eae9e9c9c5c5/protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", size = 392745, upload-time = "2025-05-28T14:22:10.524Z" }, + { url = "https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", size = 413736, upload-time = "2025-05-28T14:22:13.156Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", size = 394537, upload-time = "2025-05-28T14:22:14.768Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", size = 294005, upload-time = "2025-05-28T14:22:16.052Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", size = 294924, upload-time = "2025-05-28T14:22:17.105Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757, upload-time = "2025-05-28T14:22:24.135Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "rabinmiller" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/9a4bd1d823200b4fcbdc25584cf4e788f672cdf0d6622b66a8b49c3be925/rabinmiller-0.1.0.tar.gz", hash = "sha256:a9873aa6fdd0c26d5205d99e126fd94e6e1bb2aa966e167e136dfbfab0d0556d", size = 5159, upload-time = "2024-11-22T07:14:04.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/b0/68c2efd5f025b80316fce28e49ce25c5d0171aa17ce7f94a89c0a6544d2b/rabinmiller-0.1.0-py3-none-any.whl", hash = "sha256:3fec2d26fc210772ced965a8f0e2870e5582cadf255bc665ef3f4932752ada5f", size = 5309, upload-time = "2024-11-22T07:14:03.572Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/60184b7fc00dd3ca80ac635dd5b8577d444c57e8e8742cecabfacb829921/rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", size = 27304, upload-time = "2025-05-21T12:46:12.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/81/28ab0408391b1dc57393653b6a0cf2014cc282cc2909e4615e63e58262be/rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c", size = 364647, upload-time = "2025-05-21T12:43:28.559Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9a/7797f04cad0d5e56310e1238434f71fc6939d0bc517192a18bb99a72a95f/rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b", size = 350454, upload-time = "2025-05-21T12:43:30.615Z" }, + { url = "https://files.pythonhosted.org/packages/69/3c/93d2ef941b04898011e5d6eaa56a1acf46a3b4c9f4b3ad1bbcbafa0bee1f/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa", size = 389665, upload-time = "2025-05-21T12:43:32.629Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/ad0e31e928751dde8903a11102559628d24173428a0f85e25e187defb2c1/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda", size = 403873, upload-time = "2025-05-21T12:43:34.576Z" }, + { url = "https://files.pythonhosted.org/packages/16/ad/c0c652fa9bba778b4f54980a02962748479dc09632e1fd34e5282cf2556c/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309", size = 525866, upload-time = "2025-05-21T12:43:36.123Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/3e1839bc527e6fcf48d5fec4770070f872cdee6c6fbc9b259932f4e88a38/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b", size = 416886, upload-time = "2025-05-21T12:43:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/7a/95/dd6b91cd4560da41df9d7030a038298a67d24f8ca38e150562644c829c48/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea", size = 390666, upload-time = "2025-05-21T12:43:40.065Z" }, + { url = "https://files.pythonhosted.org/packages/64/48/1be88a820e7494ce0a15c2d390ccb7c52212370badabf128e6a7bb4cb802/rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65", size = 425109, upload-time = "2025-05-21T12:43:42.263Z" }, + { url = "https://files.pythonhosted.org/packages/cf/07/3e2a17927ef6d7720b9949ec1b37d1e963b829ad0387f7af18d923d5cfa5/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c", size = 567244, upload-time = "2025-05-21T12:43:43.846Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e5/76cf010998deccc4f95305d827847e2eae9c568099c06b405cf96384762b/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd", size = 596023, upload-time = "2025-05-21T12:43:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/52/9a/df55efd84403736ba37a5a6377b70aad0fd1cb469a9109ee8a1e21299a1c/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb", size = 561634, upload-time = "2025-05-21T12:43:48.263Z" }, + { url = "https://files.pythonhosted.org/packages/ab/aa/dc3620dd8db84454aaf9374bd318f1aa02578bba5e567f5bf6b79492aca4/rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe", size = 222713, upload-time = "2025-05-21T12:43:49.897Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/7cef485269a50ed5b4e9bae145f512d2a111ca638ae70cc101f661b4defd/rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192", size = 235280, upload-time = "2025-05-21T12:43:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/99/f2/c2d64f6564f32af913bf5f3f7ae41c7c263c5ae4c4e8f1a17af8af66cd46/rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728", size = 225399, upload-time = "2025-05-21T12:43:53.351Z" }, + { url = "https://files.pythonhosted.org/packages/2b/da/323848a2b62abe6a0fec16ebe199dc6889c5d0a332458da8985b2980dffe/rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559", size = 364498, upload-time = "2025-05-21T12:43:54.841Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b4/4d3820f731c80fd0cd823b3e95b9963fec681ae45ba35b5281a42382c67d/rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1", size = 350083, upload-time = "2025-05-21T12:43:56.428Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b1/3a8ee1c9d480e8493619a437dec685d005f706b69253286f50f498cbdbcf/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c", size = 389023, upload-time = "2025-05-21T12:43:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/3b/31/17293edcfc934dc62c3bf74a0cb449ecd549531f956b72287203e6880b87/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb", size = 403283, upload-time = "2025-05-21T12:43:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ca/e0f0bc1a75a8925024f343258c8ecbd8828f8997ea2ac71e02f67b6f5299/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40", size = 524634, upload-time = "2025-05-21T12:44:01.087Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/5d0be919037178fff33a6672ffc0afa04ea1cfcb61afd4119d1b5280ff0f/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79", size = 416233, upload-time = "2025-05-21T12:44:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325", size = 390375, upload-time = "2025-05-21T12:44:04.162Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ac/a87f339f0e066b9535074a9f403b9313fd3892d4a164d5d5f5875ac9f29f/rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295", size = 424537, upload-time = "2025-05-21T12:44:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8d5c1567eaf8c8afe98a838dd24de5013ce6e8f53a01bd47fe8bb06b5533/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b", size = 566425, upload-time = "2025-05-21T12:44:08.242Z" }, + { url = "https://files.pythonhosted.org/packages/95/33/03016a6be5663b389c8ab0bbbcca68d9e96af14faeff0a04affcb587e776/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98", size = 595197, upload-time = "2025-05-21T12:44:10.449Z" }, + { url = "https://files.pythonhosted.org/packages/33/8d/da9f4d3e208c82fda311bff0cf0a19579afceb77cf456e46c559a1c075ba/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd", size = 561244, upload-time = "2025-05-21T12:44:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b3/39d5dcf7c5f742ecd6dbc88f6f84ae54184b92f5f387a4053be2107b17f1/rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31", size = 222254, upload-time = "2025-05-21T12:44:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/5f/19/2d6772c8eeb8302c5f834e6d0dfd83935a884e7c5ce16340c7eaf89ce925/rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500", size = 234741, upload-time = "2025-05-21T12:44:16.236Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/145ada26cfaf86018d0eb304fe55eafdd4f0b6b84530246bb4a7c4fb5c4b/rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5", size = 224830, upload-time = "2025-05-21T12:44:17.749Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ca/d435844829c384fd2c22754ff65889c5c556a675d2ed9eb0e148435c6690/rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129", size = 359668, upload-time = "2025-05-21T12:44:19.322Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/b056f21db3a09f89410d493d2f6614d87bb162499f98b649d1dbd2a81988/rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d", size = 345649, upload-time = "2025-05-21T12:44:20.962Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0f/e0d00dc991e3d40e03ca36383b44995126c36b3eafa0ccbbd19664709c88/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72", size = 384776, upload-time = "2025-05-21T12:44:22.516Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a2/59374837f105f2ca79bde3c3cd1065b2f8c01678900924949f6392eab66d/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34", size = 395131, upload-time = "2025-05-21T12:44:24.147Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dc/48e8d84887627a0fe0bac53f0b4631e90976fd5d35fff8be66b8e4f3916b/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9", size = 520942, upload-time = "2025-05-21T12:44:25.915Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f5/ee056966aeae401913d37befeeab57a4a43a4f00099e0a20297f17b8f00c/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5", size = 411330, upload-time = "2025-05-21T12:44:27.638Z" }, + { url = "https://files.pythonhosted.org/packages/ab/74/b2cffb46a097cefe5d17f94ede7a174184b9d158a0aeb195f39f2c0361e8/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194", size = 387339, upload-time = "2025-05-21T12:44:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9a/0ff0b375dcb5161c2b7054e7d0b7575f1680127505945f5cabaac890bc07/rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6", size = 418077, upload-time = "2025-05-21T12:44:30.877Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a1/fda629bf20d6b698ae84c7c840cfb0e9e4200f664fc96e1f456f00e4ad6e/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78", size = 562441, upload-time = "2025-05-21T12:44:32.541Z" }, + { url = "https://files.pythonhosted.org/packages/20/15/ce4b5257f654132f326f4acd87268e1006cc071e2c59794c5bdf4bebbb51/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72", size = 590750, upload-time = "2025-05-21T12:44:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ab/e04bf58a8d375aeedb5268edcc835c6a660ebf79d4384d8e0889439448b0/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66", size = 558891, upload-time = "2025-05-21T12:44:37.358Z" }, + { url = "https://files.pythonhosted.org/packages/90/82/cb8c6028a6ef6cd2b7991e2e4ced01c854b6236ecf51e81b64b569c43d73/rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523", size = 218718, upload-time = "2025-05-21T12:44:38.969Z" }, + { url = "https://files.pythonhosted.org/packages/b6/97/5a4b59697111c89477d20ba8a44df9ca16b41e737fa569d5ae8bff99e650/rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763", size = 232218, upload-time = "2025-05-21T12:44:40.512Z" }, +] + +[[package]] +name = "ruff" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987, upload-time = "2025-09-18T19:52:44.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308, upload-time = "2025-09-18T19:51:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258, upload-time = "2025-09-18T19:52:00.184Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554, upload-time = "2025-09-18T19:52:02.753Z" }, + { url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181, upload-time = "2025-09-18T19:52:05.279Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599, upload-time = "2025-09-18T19:52:07.497Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178, upload-time = "2025-09-18T19:52:10.189Z" }, + { url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474, upload-time = "2025-09-18T19:52:12.866Z" }, + { url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531, upload-time = "2025-09-18T19:52:15.245Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267, upload-time = "2025-09-18T19:52:17.649Z" }, + { url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120, upload-time = "2025-09-18T19:52:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084, upload-time = "2025-09-18T19:52:23.032Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105, upload-time = "2025-09-18T19:52:25.263Z" }, + { url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284, upload-time = "2025-09-18T19:52:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314, upload-time = "2025-09-18T19:52:30.212Z" }, + { url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360, upload-time = "2025-09-18T19:52:32.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448, upload-time = "2025-09-18T19:52:35.545Z" }, + { url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458, upload-time = "2025-09-18T19:52:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893, upload-time = "2025-09-18T19:52:41.283Z" }, +] + +[[package]] +name = "secp256k1" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/41/bb668a6e4192303542d2d90c3b38d564af3c17c61bd7d4039af4f29405fe/secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397", size = 2420607, upload-time = "2021-11-06T01:36:10.707Z" } + +[[package]] +name = "secretvaults" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "blindfold" }, + { name = "nuc" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "structlog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/a2577091e6cd8caed0d313e39600b4f6851be66ffa3ef36fac8932dd0d7b/secretvaults-0.2.1.tar.gz", hash = "sha256:819167ac4a992185fcbd3454d0f735a2fec0918a573911e6da628fb03e853e39", size = 28241, upload-time = "2025-08-18T14:30:28.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/1c/2ffc9365baace0e9f64d0280074704657249c0093c8ed04974e9f458dfe9/secretvaults-0.2.1-py3-none-any.whl", hash = "sha256:d3fbc844f3439ab393a63722886234e1731bdd2170213233f2a0fd9d04031bd2", size = 35438, upload-time = "2025-08-18T14:30:27.013Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "structlog" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b9/6e672db4fec07349e7a8a8172c1a6ae235c58679ca29c3f86a61b5e59ff3/structlog-25.4.0.tar.gz", hash = "sha256:186cd1b0a8ae762e29417095664adf1d6a31702160a46dacb7796ea82f7409e4", size = 1369138, upload-time = "2025-06-02T08:21:12.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/4a/97ee6973e3a73c74c8120d59829c3861ea52210667ec3e7a16045c62b64d/structlog-25.4.0-py3-none-any.whl", hash = "sha256:fe809ff5c27e557d14e613f45ca441aabda051d119ee5a0102aaba6ce40eed2c", size = 68720, upload-time = "2025-06-02T08:21:11.43Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +] diff --git a/db/.gitignore b/db/.gitignore deleted file mode 100644 index 6a91a439..00000000 --- a/db/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.sqlite \ No newline at end of file diff --git a/db/README.md b/db/README.md deleted file mode 100644 index 45b7c5c8..00000000 --- a/db/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# DB - -This directory is meant to host the db data. \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 95815f33..fe4ed99d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -33,20 +33,10 @@ services: condition: service_healthy nilauth-credit-server: condition: service_healthy - environment: - - POSTGRES_DB=${POSTGRES_DB_NUC} volumes: - ./nilai-api/:/app/nilai-api/ - ./packages/:/app/packages/ - ./nilai-auth/nuc-helpers/:/app/nilai-auth/nuc-helpers/ - attestation: - ports: - - "8081:8080" - env_file: - - .env - volumes: - - ./nilai-attestation/:/app/nilai-attestation/ - - ./packages/:/app/packages/ redis: ports: - "6379:6379" @@ -68,6 +58,18 @@ services: start_period: 10s timeout: 10s grafana: + container_name: grafana + image: 'grafana/grafana:11.5.1' + restart: unless-stopped + user: "$UID:$GID" + environment: + - GF_USERS_ALLOW_SIGN_UP=false + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:3000/api/health" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s env_file: - .env ports: @@ -77,10 +79,6 @@ services: - ${PWD}/grafana/datasources/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml - ${PWD}/grafana/dashboards/filesystem.yml:/etc/grafana/provisioning/dashboards/filesystem.yml - ${PWD}/grafana/config/grafana.ini:/etc/grafana/grafana.ini - prometheus: - volumes: - - ${PWD}/prometheus/config/prometheus.yml:/etc/prometheus/prometheus.yml - - ${PWD}/prometheus/data:/prometheus/data nilauth-postgres: image: postgres:16-alpine environment: @@ -96,7 +94,8 @@ services: retries: 5 nilauth-credit-server: - image: ghcr.io/nillionnetwork/nilauth-credit:sha-cb9e36a + image: ghcr.io/nillionnetwork/nilauth-credit:sha-6754a1d + platform: linux/amd64 # for macOS to force running on Rosetta 2 environment: DATABASE_URL: postgresql://nilauth:nilauth_dev_password@nilauth-postgres:5432/nilauth_credit HOST: 0.0.0.0 @@ -106,8 +105,10 @@ services: depends_on: nilauth-postgres: condition: service_healthy + volumes: + - ./scripts/credit-init.sql:/app/migrations/20251015000006_seed_test_data.sql healthcheck: - test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health" ] + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/health" ] interval: 30s retries: 3 start_period: 15s diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d2812d92..7364a353 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,17 +1,5 @@ services: - prometheus: - volumes: - - ${FILES}/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - - prometheus_data:/prometheus/data - grafana: - env_file: - - .env.mainnet - volumes: - - ${FILES}/grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml - - ${FILES}/grafana/filesystem.yml:/etc/grafana/provisioning/dashboards/filesystem.yml - - ${FILES}/grafana/grafana.ini:/etc/grafana/grafana.ini - - ${FILES}/grafana/nuc-query-data.json:/var/lib/grafana/dashboards/nuc-query-data.json - - ${FILES}/grafana/query-data.json:/var/lib/grafana/dashboards/query-data.json + api: env_file: - .env.mainnet @@ -29,15 +17,5 @@ services: - .env.mainnet volumes: - ${FILES}/caddy/caddyfile:/etc/caddy/Caddyfile - attestation: - env_file: - - .env.mainnet - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: 1 - capabilities: [gpu] #volumes: # - /dev/sev-guest:/dev/sev-guest # for AMD SEV diff --git a/docker-compose.testnet.prod.yml b/docker-compose.testnet.prod.yml index a31f37cf..1525bbcf 100644 --- a/docker-compose.testnet.prod.yml +++ b/docker-compose.testnet.prod.yml @@ -10,8 +10,3 @@ services: - AUTH_STRATEGY=nuc volumes: - ${FILES}/testnet/nilai-api/config.yaml:/app/nilai-api/src/nilai_api/config/config.yaml - grafana: - env_file: - - .env.mainnet - volumes: - - ${FILES}/grafana/testnet-nuc-query-data.json:/var/lib/grafana/dashboards/testnet-nuc-query-data.json diff --git a/docker-compose.testnet.yml b/docker-compose.testnet.yml index 3911b67d..1779045c 100644 --- a/docker-compose.testnet.yml +++ b/docker-compose.testnet.yml @@ -8,7 +8,7 @@ services: container_name: testnet-nilai-nuc-api image: nillion/nilai-api:latest depends_on: - etcd: + redis: condition: service_healthy restart: unless-stopped healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index 86f9fa11..7f4d0233 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,4 @@ services: - etcd: - container_name: etcd - image: 'bitnamilegacy/etcd' - environment: - - ALLOW_NONE_AUTHENTICATION=yes - - ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379 - restart: unless-stopped - healthcheck: - test: ["CMD", "etcdctl", "endpoint", "health"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 5s redis: container_name: redis image: 'redis:latest' @@ -22,56 +9,11 @@ services: timeout: 10s retries: 3 start_period: 5s - - prometheus: - container_name: prometheus - image: prom/prometheus:v3.1.0 - restart: unless-stopped - user: "$UID:$GID" - command: "--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.retention.time=30d --web.enable-admin-api" - healthcheck: - test: ["CMD", "wget", "http://localhost:9090/-/healthy", "-O", "/dev/null", "-o", "/dev/null"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - node_exporter: - container_name: node-exporter - image: quay.io/prometheus/node-exporter:v1.8.2 - command: - - '--path.rootfs=/host' - restart: unless-stopped -# volumes: -# - '/:/host:ro,rslave' - healthcheck: - test: ["CMD", "wget", "http://localhost:9100/", "-O", "/dev/null", "-o", "/dev/null"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - grafana: - container_name: grafana - image: 'grafana/grafana:11.5.1' - restart: unless-stopped - user: "$UID:$GID" - depends_on: - - prometheus - environment: - - GF_USERS_ALLOW_SIGN_UP=false - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - api: container_name: nilai-api image: nillion/nilai-api:latest depends_on: - etcd: + redis: condition: service_healthy restart: unless-stopped healthcheck: @@ -84,7 +26,7 @@ services: container_name: nilai-nuc-api image: nillion/nilai-api:latest depends_on: - etcd: + redis: condition: service_healthy api: condition: service_healthy @@ -97,16 +39,6 @@ services: retries: 3 start_period: 15s timeout: 10s - attestation: - image: nillion/nilai-attestation:latest - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 30s - retries: 3 - start_period: 15s - timeout: 10s - caddy: image: caddy:latest container_name: caddy diff --git a/docker/compose/docker-compose.deepseek-14b-gpu.yml b/docker/compose/docker-compose.deepseek-14b-gpu.yml index 7c271257..187c4225 100644 --- a/docker/compose/docker-compose.deepseek-14b-gpu.yml +++ b/docker/compose/docker-compose.deepseek-14b-gpu.yml @@ -14,11 +14,6 @@ services: env_file: - .env restart: unless-stopped - depends_on: - etcd: - condition: service_healthy - llama_8b_gpu: - condition: service_healthy command: > --model deepseek-ai/DeepSeek-R1-Distill-Qwen-14B --gpu-memory-utilization 0.39 @@ -29,8 +24,8 @@ services: environment: - SVC_HOST=deepseek_14b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=false volumes: - hugging_face_models:/root/.cache/huggingface # cache models diff --git a/docker/compose/docker-compose.dolphin-8b-gpu.yml b/docker/compose/docker-compose.dolphin-8b-gpu.yml deleted file mode 100644 index 95402a7f..00000000 --- a/docker/compose/docker-compose.dolphin-8b-gpu.yml +++ /dev/null @@ -1,46 +0,0 @@ -services: - dolphin_8b_gpu: - image: nillion/nilai-vllm:latest - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: all - capabilities: [gpu] - ulimits: - memlock: -1 - stack: 67108864 - env_file: - - .env - restart: unless-stopped - depends_on: - etcd: - condition: service_healthy - llama_3b_gpu: - condition: service_healthy - command: > - --model cognitivecomputations/Dolphin3.0-Llama3.1-8B - --gpu-memory-utilization 0.21 - --max-model-len 10000 - --max-num-batched-tokens 10000 - --tensor-parallel-size 1 - --enable-auto-tool-choice - --tool-call-parser llama3_json - --uvicorn-log-level warning - environment: - - SVC_HOST=dolphin_8b_gpu - - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 - - TOOL_SUPPORT=true - volumes: - - hugging_face_models:/root/.cache/huggingface # cache models - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/health"] - interval: 30s - retries: 3 - start_period: 60s - timeout: 10s -volumes: - hugging_face_models: diff --git a/docker/compose/docker-compose.gemma-27b-gpu.yml b/docker/compose/docker-compose.gemma-27b-gpu.yml index db970525..185a4855 100644 --- a/docker/compose/docker-compose.gemma-27b-gpu.yml +++ b/docker/compose/docker-compose.gemma-27b-gpu.yml @@ -16,7 +16,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model google/gemma-3-27b-it @@ -29,8 +29,8 @@ services: environment: - SVC_HOST=gemma_27b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=false - MULTIMODAL_SUPPORT=true - MODEL_NUM_RETRIES=60 diff --git a/docker/compose/docker-compose.gemma-4b-gpu.ci.yml b/docker/compose/docker-compose.gemma-4b-gpu.ci.yml index 29423275..3302d82f 100644 --- a/docker/compose/docker-compose.gemma-4b-gpu.ci.yml +++ b/docker/compose/docker-compose.gemma-4b-gpu.ci.yml @@ -17,7 +17,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model google/gemma-3-4b-it @@ -28,8 +28,8 @@ services: environment: - SVC_HOST=gemma_4b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=false - MULTIMODAL_SUPPORT=true - CUDA_LAUNCH_BLOCKING=1 diff --git a/docker/compose/docker-compose.gpt-120b-gpu.yml b/docker/compose/docker-compose.gpt-120b-gpu.yml index 2586179c..3ab05936 100644 --- a/docker/compose/docker-compose.gpt-120b-gpu.yml +++ b/docker/compose/docker-compose.gpt-120b-gpu.yml @@ -16,7 +16,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model openai/gpt-oss-120b @@ -28,8 +28,8 @@ services: environment: - SVC_HOST=gpt_120b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface # cache models diff --git a/docker/compose/docker-compose.gpt-20b-gpu.ci.yml b/docker/compose/docker-compose.gpt-20b-gpu.ci.yml index dcfef4cb..5fb24352 100644 --- a/docker/compose/docker-compose.gpt-20b-gpu.ci.yml +++ b/docker/compose/docker-compose.gpt-20b-gpu.ci.yml @@ -7,7 +7,7 @@ services: devices: - driver: nvidia count: 1 - capabilities: [gpu] + capabilities: [ gpu ] ulimits: memlock: -1 @@ -16,27 +16,20 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > - --model openai/gpt-oss-20b - --gpu-memory-utilization 0.95 - --max-model-len 10000 - --max-num-batched-tokens 10000 - --max-num-seqs 2 - --tensor-parallel-size 1 - --uvicorn-log-level warning - --async-scheduling + --model openai/gpt-oss-20b --gpu-memory-utilization 0.95 --max-model-len 10000 --max-num-batched-tokens 10000 --max-num-seqs 2 --tensor-parallel-size 1 --uvicorn-log-level warning --async-scheduling environment: - SVC_HOST=gpt_20b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - - hugging_face_models:/root/.cache/huggingface # cache models + - hugging_face_models:/root/.cache/huggingface # cache models healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + test: [ "CMD", "curl", "-f", "http://localhost:8000/health" ] interval: 30s retries: 10 start_period: 900s diff --git a/docker/compose/docker-compose.gpt-20b-gpu.yml b/docker/compose/docker-compose.gpt-20b-gpu.yml index 37b9b5fa..9694249c 100644 --- a/docker/compose/docker-compose.gpt-20b-gpu.yml +++ b/docker/compose/docker-compose.gpt-20b-gpu.yml @@ -16,7 +16,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model openai/gpt-oss-20b @@ -28,8 +28,8 @@ services: environment: - SVC_HOST=gpt_20b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface # cache models diff --git a/docker/compose/docker-compose.llama-1b-cpu.yml b/docker/compose/docker-compose.llama-1b-cpu.yml index 50063464..bc402f01 100644 --- a/docker/compose/docker-compose.llama-1b-cpu.yml +++ b/docker/compose/docker-compose.llama-1b-cpu.yml @@ -6,7 +6,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model meta-llama/Llama-3.2-1B-Instruct @@ -19,11 +19,13 @@ services: environment: - SVC_HOST=llama_1b_cpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface + - ./nilai-models/:/daemon/nilai-models/ + - ./packages/:/daemon/packages/ healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s diff --git a/docker/compose/docker-compose.llama-1b-gpu.ci.yml b/docker/compose/docker-compose.llama-1b-gpu.ci.yml index cca105f7..eaa9ee33 100644 --- a/docker/compose/docker-compose.llama-1b-gpu.ci.yml +++ b/docker/compose/docker-compose.llama-1b-gpu.ci.yml @@ -17,7 +17,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model meta-llama/Llama-3.2-1B-Instruct @@ -32,8 +32,8 @@ services: environment: - SVC_HOST=llama_1b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true - CUDA_LAUNCH_BLOCKING=1 volumes: diff --git a/docker/compose/docker-compose.llama-1b-gpu.yml b/docker/compose/docker-compose.llama-1b-gpu.yml index ebbedea7..356913d0 100644 --- a/docker/compose/docker-compose.llama-1b-gpu.yml +++ b/docker/compose/docker-compose.llama-1b-gpu.yml @@ -16,7 +16,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model meta-llama/Llama-3.2-1B-Instruct @@ -30,8 +30,8 @@ services: environment: - SVC_HOST=llama_1b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface # cache models diff --git a/docker/compose/docker-compose.llama-3b-gpu.prod.yml b/docker/compose/docker-compose.llama-3b-gpu.prod.yml deleted file mode 100644 index 4a37ace5..00000000 --- a/docker/compose/docker-compose.llama-3b-gpu.prod.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - llama_3b_gpu: - depends_on: - deepseek_14b_gpu: - condition: service_healthy \ No newline at end of file diff --git a/docker/compose/docker-compose.llama-3b-gpu.yml b/docker/compose/docker-compose.llama-3b-gpu.yml index 724ad42e..35869ad4 100644 --- a/docker/compose/docker-compose.llama-3b-gpu.yml +++ b/docker/compose/docker-compose.llama-3b-gpu.yml @@ -16,7 +16,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model meta-llama/Llama-3.2-3B-Instruct @@ -30,8 +30,8 @@ services: environment: - SVC_HOST=llama_3b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface # cache models diff --git a/docker/compose/docker-compose.llama-70b-gpu.yml b/docker/compose/docker-compose.llama-70b-gpu.yml index 55b0b4a9..3a70d49d 100644 --- a/docker/compose/docker-compose.llama-70b-gpu.yml +++ b/docker/compose/docker-compose.llama-70b-gpu.yml @@ -16,7 +16,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model hugging-quants/Meta-Llama-3.1-70B-Instruct-AWQ-INT4 @@ -30,8 +30,8 @@ services: environment: - SVC_HOST=llama_70b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface # cache models diff --git a/docker/compose/docker-compose.llama-8b-gpu.yml b/docker/compose/docker-compose.llama-8b-gpu.yml index 61b7a1c8..b9d1d00f 100644 --- a/docker/compose/docker-compose.llama-8b-gpu.yml +++ b/docker/compose/docker-compose.llama-8b-gpu.yml @@ -16,7 +16,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model meta-llama/Llama-3.1-8B-Instruct @@ -27,12 +27,12 @@ services: --tool-call-parser llama3_json --uvicorn-log-level warning --enable-auto-tool-choice - --chat-template /opt/vllm/templates/llama3.1_tool_json.jinja + --chat-template /daemon/nilai-models/templates/llama3.1_tool_json.jinja environment: - SVC_HOST=llama_8b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface diff --git a/docker/compose/docker-compose.lmstudio.yml b/docker/compose/docker-compose.lmstudio.yml index aaa25259..ac01134c 100644 --- a/docker/compose/docker-compose.lmstudio.yml +++ b/docker/compose/docker-compose.lmstudio.yml @@ -4,13 +4,13 @@ services: container_name: nilai-lmstudio-announcer restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy environment: - SVC_HOST=host.docker.internal - SVC_PORT=1234 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - LMSTUDIO_SUPPORTED_FEATURES=chat_completion extra_hosts: - "host.docker.internal:host-gateway" diff --git a/docker/compose/docker-compose.nilai-prod-1.yml b/docker/compose/docker-compose.nilai-prod-1.yml index 75d9b50c..10c7c46e 100644 --- a/docker/compose/docker-compose.nilai-prod-1.yml +++ b/docker/compose/docker-compose.nilai-prod-1.yml @@ -16,7 +16,7 @@ services: - .env.mainnet restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model google/gemma-3-27b-it @@ -29,8 +29,8 @@ services: environment: - SVC_HOST=gemma_27b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=false - MULTIMODAL_SUPPORT=true - MODEL_NUM_RETRIES=60 diff --git a/docker/compose/docker-compose.nilai-prod-2.yml b/docker/compose/docker-compose.nilai-prod-2.yml index d9da5758..e48d266e 100644 --- a/docker/compose/docker-compose.nilai-prod-2.yml +++ b/docker/compose/docker-compose.nilai-prod-2.yml @@ -16,7 +16,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy gpt_20b_gpu: # Llama takes less time to initialize @@ -33,12 +33,12 @@ services: --tool-call-parser llama3_json --uvicorn-log-level warning --enable-auto-tool-choice - --chat-template /opt/vllm/templates/llama3.1_tool_json.jinja + --chat-template /daemon/nilai-models/templates/llama3.1_tool_json.jinja environment: - SVC_HOST=llama_8b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface @@ -65,7 +65,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: > --model openai/gpt-oss-20b @@ -77,8 +77,8 @@ services: environment: - SVC_HOST=gpt_20b_gpu - SVC_PORT=8000 - - ETCD_HOST=etcd - - ETCD_PORT=2379 + - DISCOVERY_HOST=redis + - DISCOVERY_PORT=6379 - TOOL_SUPPORT=true volumes: - hugging_face_models:/root/.cache/huggingface # cache models diff --git a/docker/compose/docker-compose.qwen-2b-gpu.ci.yml b/docker/compose/docker-compose.qwen-2b-gpu.ci.yml index 7d040caf..fad095a5 100644 --- a/docker/compose/docker-compose.qwen-2b-gpu.ci.yml +++ b/docker/compose/docker-compose.qwen-2b-gpu.ci.yml @@ -18,7 +18,7 @@ services: - .env restart: unless-stopped depends_on: - etcd: + redis: condition: service_healthy command: [ @@ -44,8 +44,8 @@ services: environment: SVC_HOST: qwen2vl_2b_gpu SVC_PORT: "8000" - ETCD_HOST: etcd - ETCD_PORT: "2379" + DISCOVERY_HOST: redis + DISCOVERY_PORT: "6379" TOOL_SUPPORT: "true" MULTIMODAL_SUPPORT: "true" CUDA_LAUNCH_BLOCKING: "1" diff --git a/docker/vllm.Dockerfile b/docker/vllm.Dockerfile index eb938667..687a0bd5 100644 --- a/docker/vllm.Dockerfile +++ b/docker/vllm.Dockerfile @@ -10,7 +10,6 @@ FROM vllm/vllm-openai:v0.10.1 # ENV EXEC_PATH=nilai_models.models.${MODEL_NAME}:app COPY --link . /daemon/ -COPY --link vllm_templates /opt/vllm/templates WORKDIR /daemon/nilai-models/ diff --git a/grafana/runtime-data/dashboards/nuc-query-data.json b/grafana/runtime-data/dashboards/nuc-query-data.json index d66fd428..c7bbb6b2 100644 --- a/grafana/runtime-data/dashboards/nuc-query-data.json +++ b/grafana/runtime-data/dashboards/nuc-query-data.json @@ -126,7 +126,7 @@ "editorMode": "code", "format": "time_series", "rawQuery": true, - "rawSql": "SELECT \n date_trunc('${time_granularity}', q.query_timestamp) AS \"time\", \n COUNT(q.id) AS \"Queries\"\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:value}' = 'All' OR u.name = '${user_filter:value}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY date_trunc('${time_granularity}', q.query_timestamp)\nORDER BY \"time\";", + "rawSql": "SELECT \n date_trunc('${time_granularity}', q.query_timestamp) AS \"time\", \n COUNT(q.id) AS \"Queries\"\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:value}' = 'All' OR u.name = '${user_filter:value}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY date_trunc('${time_granularity}', q.query_timestamp)\nORDER BY \"time\";", "refId": "A", "sql": { "columns": [ @@ -218,7 +218,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n q.model, \n COUNT(q.id) AS total_queries\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\nGROUP BY q.model\nORDER BY total_queries DESC;", + "rawSql": "SELECT \n q.model, \n COUNT(q.id) AS total_queries\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\nGROUP BY q.model\nORDER BY total_queries DESC;", "refId": "A", "sql": { "columns": [ @@ -352,7 +352,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 12 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name\nORDER BY \"Queries\" DESC;", + "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 12 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name\nORDER BY \"Queries\" DESC;", "refId": "A", "sql": { "columns": [ @@ -360,7 +360,7 @@ "alias": "\"User\"", "parameters": [ { - "name": "userid", + "name": "user_id", "type": "functionParameter" } ], @@ -381,7 +381,7 @@ "groupBy": [ { "property": { - "name": "userid", + "name": "user_id", "type": "string" }, "type": "groupBy" @@ -481,7 +481,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 8 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\",\n q.model AS \"Model\",\n COUNT(q.id) AS \"Queries\",\n MIN(q.query_timestamp) AS \"First Query\",\n MAX(q.query_timestamp) AS \"Last Query\"\nFROM query_logs q \nJOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name, q.model\nORDER BY \"Queries\" DESC\nLIMIT 20;", + "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 8 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\",\n q.model AS \"Model\",\n COUNT(q.id) AS \"Queries\",\n MIN(q.query_timestamp) AS \"First Query\",\n MAX(q.query_timestamp) AS \"Last Query\"\nFROM query_logs q \nJOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name, q.model\nORDER BY \"Queries\" DESC\nLIMIT 20;", "refId": "A", "sql": { "columns": [ diff --git a/grafana/runtime-data/dashboards/query-data.json b/grafana/runtime-data/dashboards/query-data.json index 8e0b774f..f33f87a8 100644 --- a/grafana/runtime-data/dashboards/query-data.json +++ b/grafana/runtime-data/dashboards/query-data.json @@ -126,7 +126,7 @@ "editorMode": "code", "format": "time_series", "rawQuery": true, - "rawSql": "SELECT \n date_trunc('${time_granularity}', q.query_timestamp) AS \"time\", \n COUNT(q.id) AS \"Queries\"\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:value}' = 'All' OR u.name = '${user_filter:value}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY date_trunc('${time_granularity}', q.query_timestamp)\nORDER BY \"time\";", + "rawSql": "SELECT \n date_trunc('${time_granularity}', q.query_timestamp) AS \"time\", \n COUNT(q.id) AS \"Queries\"\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:value}' = 'All' OR u.name = '${user_filter:value}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY date_trunc('${time_granularity}', q.query_timestamp)\nORDER BY \"time\";", "refId": "A", "sql": { "columns": [ @@ -218,7 +218,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n q.model, \n COUNT(q.id) AS total_queries\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\nGROUP BY q.model\nORDER BY total_queries DESC;", + "rawSql": "SELECT \n q.model, \n COUNT(q.id) AS total_queries\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\nGROUP BY q.model\nORDER BY total_queries DESC;", "refId": "A", "sql": { "columns": [ @@ -352,7 +352,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 12 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name\nORDER BY \"Queries\" DESC;", + "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 12 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name\nORDER BY \"Queries\" DESC;", "refId": "A", "sql": { "columns": [ @@ -360,7 +360,7 @@ "alias": "\"User\"", "parameters": [ { - "name": "userid", + "name": "user_id", "type": "functionParameter" } ], @@ -381,7 +381,7 @@ "groupBy": [ { "property": { - "name": "userid", + "name": "user_id", "type": "string" }, "type": "groupBy" @@ -481,7 +481,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 8 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\",\n q.model AS \"Model\",\n COUNT(q.id) AS \"Queries\",\n MIN(q.query_timestamp) AS \"First Query\",\n MAX(q.query_timestamp) AS \"Last Query\"\nFROM query_logs q \nJOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name, q.model\nORDER BY \"Queries\" DESC\nLIMIT 20;", + "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 8 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\",\n q.model AS \"Model\",\n COUNT(q.id) AS \"Queries\",\n MIN(q.query_timestamp) AS \"First Query\",\n MAX(q.query_timestamp) AS \"Last Query\"\nFROM query_logs q \nJOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name, q.model\nORDER BY \"Queries\" DESC\nLIMIT 20;", "refId": "A", "sql": { "columns": [ diff --git a/grafana/runtime-data/dashboards/testnet-nuc-query-data.json b/grafana/runtime-data/dashboards/testnet-nuc-query-data.json index f98d70e9..358ba4eb 100644 --- a/grafana/runtime-data/dashboards/testnet-nuc-query-data.json +++ b/grafana/runtime-data/dashboards/testnet-nuc-query-data.json @@ -126,7 +126,7 @@ "editorMode": "code", "format": "time_series", "rawQuery": true, - "rawSql": "SELECT \n date_trunc('${time_granularity}', q.query_timestamp) AS \"time\", \n COUNT(q.id) AS \"Queries\"\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:value}' = 'All' OR u.name = '${user_filter:value}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY date_trunc('${time_granularity}', q.query_timestamp)\nORDER BY \"time\";", + "rawSql": "SELECT \n date_trunc('${time_granularity}', q.query_timestamp) AS \"time\", \n COUNT(q.id) AS \"Queries\"\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:value}' = 'All' OR u.name = '${user_filter:value}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY date_trunc('${time_granularity}', q.query_timestamp)\nORDER BY \"time\";", "refId": "A", "sql": { "columns": [ @@ -218,7 +218,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n q.model, \n COUNT(q.id) AS total_queries\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\nGROUP BY q.model\nORDER BY total_queries DESC;", + "rawSql": "SELECT \n q.model, \n COUNT(q.id) AS total_queries\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\nGROUP BY q.model\nORDER BY total_queries DESC;", "refId": "A", "sql": { "columns": [ @@ -352,7 +352,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 12 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name\nORDER BY \"Queries\" DESC;", + "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 12 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name\nORDER BY \"Queries\" DESC;", "refId": "A", "sql": { "columns": [ @@ -360,7 +360,7 @@ "alias": "\"User\"", "parameters": [ { - "name": "userid", + "name": "user_id", "type": "functionParameter" } ], @@ -381,7 +381,7 @@ "groupBy": [ { "property": { - "name": "userid", + "name": "user_id", "type": "string" }, "type": "groupBy" @@ -481,7 +481,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 8 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\",\n q.model AS \"Model\",\n COUNT(q.id) AS \"Queries\",\n MIN(q.query_timestamp) AS \"First Query\",\n MAX(q.query_timestamp) AS \"Last Query\"\nFROM query_logs q \nJOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name, q.model\nORDER BY \"Queries\" DESC\nLIMIT 20;", + "rawSql": "SELECT \n CASE \n WHEN LENGTH(u.name) > 8 THEN LEFT(u.name, 3) || '...' || RIGHT(u.name, 3)\n ELSE u.name\n END AS \"User\",\n q.model AS \"Model\",\n COUNT(q.id) AS \"Queries\",\n MIN(q.query_timestamp) AS \"First Query\",\n MAX(q.query_timestamp) AS \"Last Query\"\nFROM query_logs q \nJOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')\n AND ('${model_filter:single}' = 'All' OR q.model = '${model_filter:single}')\nGROUP BY u.name, q.model\nORDER BY \"Queries\" DESC\nLIMIT 20;", "refId": "A", "sql": { "columns": [ diff --git a/grafana/runtime-data/dashboards/totals-data.json b/grafana/runtime-data/dashboards/totals-data.json index 2db20c7d..ff66ce0e 100644 --- a/grafana/runtime-data/dashboards/totals-data.json +++ b/grafana/runtime-data/dashboards/totals-data.json @@ -83,7 +83,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')", + "rawSql": "SELECT \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')", "refId": "A", "sql": { "columns": [ @@ -165,7 +165,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT SUM(total_tokens) AS total_tokens\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')", + "rawSql": "SELECT SUM(total_tokens) AS total_tokens\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')", "refId": "A", "sql": { "columns": [ @@ -248,7 +248,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT SUM(q.prompt_tokens) AS total_tokens\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')", + "rawSql": "SELECT SUM(q.prompt_tokens) AS total_tokens\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')", "refId": "A", "sql": { "columns": [ @@ -331,7 +331,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT SUM(q.completion_tokens) AS total_tokens\nFROM query_logs q\nLEFT JOIN users u ON q.userid = u.userid\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')", + "rawSql": "SELECT SUM(q.completion_tokens) AS total_tokens\nFROM query_logs q\nLEFT JOIN users u ON q.user_id = u.user_id\nWHERE \n q.query_timestamp >= $__timeFrom()\n AND q.query_timestamp <= $__timeTo()\n AND ('${user_filter:single}' = 'All' OR u.name = '${user_filter:single}')", "refId": "A", "sql": { "columns": [ @@ -397,4 +397,4 @@ "uid": "aex54yzf0nmyoc", "version": 1, "weekStart": "" -} \ No newline at end of file +} diff --git a/grafana/runtime-data/dashboards/usage-data.json b/grafana/runtime-data/dashboards/usage-data.json index 88857f91..a22bf914 100644 --- a/grafana/runtime-data/dashboards/usage-data.json +++ b/grafana/runtime-data/dashboards/usage-data.json @@ -299,7 +299,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n u.email AS \"User ID\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.userid = u.userid\nWHERE q.query_timestamp >= NOW() - INTERVAL '1 hours'\nGROUP BY u.email;", + "rawSql": "SELECT \n u.email AS \"User ID\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.user_id = u.user_id\nWHERE q.query_timestamp >= NOW() - INTERVAL '1 hours'\nGROUP BY u.email;", "refId": "A", "sql": { "columns": [ @@ -307,7 +307,7 @@ "alias": "\"User ID\"", "parameters": [ { - "name": "userid", + "name": "user_id", "type": "functionParameter" } ], @@ -328,7 +328,7 @@ "groupBy": [ { "property": { - "name": "userid", + "name": "user_id", "type": "string" }, "type": "groupBy" @@ -430,7 +430,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n u.email AS \"User ID\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.userid = u.userid\nWHERE q.query_timestamp >= NOW() - INTERVAL '7 days'\nGROUP BY u.email;", + "rawSql": "SELECT \n u.email AS \"User ID\", \n COUNT(q.id) AS \"Queries\" \nFROM query_logs q \nJOIN users u ON q.user_id = u.user_id\nWHERE q.query_timestamp >= NOW() - INTERVAL '7 days'\nGROUP BY u.email;", "refId": "A", "sql": { "columns": [ diff --git a/nilai-api/alembic/versions/0ba073468afc_chore_improved_database_schema.py b/nilai-api/alembic/versions/0ba073468afc_chore_improved_database_schema.py new file mode 100644 index 00000000..ebaca5a6 --- /dev/null +++ b/nilai-api/alembic/versions/0ba073468afc_chore_improved_database_schema.py @@ -0,0 +1,206 @@ +"""chore: merged database schema updates + +Revision ID: 0ba073468afc +Revises: ea942d6c7a00 +Create Date: 2025-10-31 09:43:12.022675 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "0ba073468afc" +down_revision: Union[str, None] = "9ddf28cf6b6f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### merged commands from ea942d6c7a00 and 0ba073468afc ### + # query_logs: new telemetry columns (with defaults to backfill existing rows) + op.add_column( + "query_logs", + sa.Column( + "tool_calls", sa.Integer(), server_default=sa.text("0"), nullable=False + ), + ) + op.add_column( + "query_logs", + sa.Column( + "temperature", sa.Float(), server_default=sa.text("0.9"), nullable=True + ), + ) + op.add_column( + "query_logs", + sa.Column( + "max_tokens", sa.Integer(), server_default=sa.text("4096"), nullable=True + ), + ) + op.add_column( + "query_logs", + sa.Column( + "response_time_ms", + sa.Integer(), + server_default=sa.text("-1"), + nullable=False, + ), + ) + op.add_column( + "query_logs", + sa.Column( + "model_response_time_ms", + sa.Integer(), + server_default=sa.text("-1"), + nullable=False, + ), + ) + op.add_column( + "query_logs", + sa.Column( + "tool_response_time_ms", + sa.Integer(), + server_default=sa.text("-1"), + nullable=False, + ), + ) + op.add_column( + "query_logs", + sa.Column( + "was_streamed", + sa.Boolean(), + server_default=sa.text("False"), + nullable=False, + ), + ) + op.add_column( + "query_logs", + sa.Column( + "was_multimodal", + sa.Boolean(), + server_default=sa.text("False"), + nullable=False, + ), + ) + op.add_column( + "query_logs", + sa.Column( + "was_nildb", sa.Boolean(), server_default=sa.text("False"), nullable=False + ), + ) + op.add_column( + "query_logs", + sa.Column( + "was_nilrag", sa.Boolean(), server_default=sa.text("False"), nullable=False + ), + ) + op.add_column( + "query_logs", + sa.Column( + "error_code", sa.Integer(), server_default=sa.text("200"), nullable=False + ), + ) + op.add_column( + "query_logs", + sa.Column( + "error_message", sa.Text(), server_default=sa.text("'OK'"), nullable=False + ), + ) + + # query_logs: remove FK to users.userid before dropping the column later + op.drop_constraint("query_logs_userid_fkey", "query_logs", type_="foreignkey") + + # query_logs: add lockid and index, drop legacy userid and its index + op.add_column( + "query_logs", sa.Column("lockid", sa.String(length=75), nullable=False) + ) + op.drop_index("ix_query_logs_userid", table_name="query_logs") + op.create_index( + op.f("ix_query_logs_lockid"), "query_logs", ["lockid"], unique=False + ) + op.drop_column("query_logs", "userid") + + # users: drop legacy token counters + op.drop_column("users", "prompt_tokens") + op.drop_column("users", "completion_tokens") + + # users: reshape identity columns and indexes + op.add_column("users", sa.Column("user_id", sa.String(length=75), nullable=False)) + op.drop_index("ix_users_apikey", table_name="users") + op.drop_index("ix_users_userid", table_name="users") + op.create_index(op.f("ix_users_user_id"), "users", ["user_id"], unique=False) + op.drop_column("users", "last_activity") + op.drop_column("users", "userid") + op.drop_column("users", "apikey") + op.drop_column("users", "signup_date") + op.drop_column("users", "queries") + op.drop_column("users", "name") + # ### end merged commands ### + + +def downgrade() -> None: + # ### revert merged commands back to 9ddf28cf6b6f ### + # users: restore legacy columns and indexes + op.add_column("users", sa.Column("name", sa.VARCHAR(length=100), nullable=False)) + op.add_column("users", sa.Column("queries", sa.INTEGER(), nullable=False)) + op.add_column( + "users", + sa.Column( + "signup_date", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + ) + op.add_column("users", sa.Column("apikey", sa.VARCHAR(length=75), nullable=False)) + op.add_column("users", sa.Column("userid", sa.VARCHAR(length=75), nullable=False)) + op.add_column( + "users", + sa.Column("last_activity", postgresql.TIMESTAMP(timezone=True), nullable=True), + ) + op.drop_index(op.f("ix_users_user_id"), table_name="users") + op.create_index("ix_users_userid", "users", ["userid"], unique=False) + op.create_index("ix_users_apikey", "users", ["apikey"], unique=False) + op.drop_column("users", "user_id") + op.add_column( + "users", + sa.Column( + "completion_tokens", + sa.INTEGER(), + server_default=sa.text("0"), + nullable=False, + ), + ) + op.add_column( + "users", + sa.Column( + "prompt_tokens", sa.INTEGER(), server_default=sa.text("0"), nullable=False + ), + ) + + # query_logs: restore userid, index and FK; drop new columns + op.add_column( + "query_logs", sa.Column("userid", sa.VARCHAR(length=75), nullable=False) + ) + op.drop_index(op.f("ix_query_logs_lockid"), table_name="query_logs") + op.create_index("ix_query_logs_userid", "query_logs", ["userid"], unique=False) + op.create_foreign_key( + "query_logs_userid_fkey", "query_logs", "users", ["userid"], ["userid"] + ) + op.drop_column("query_logs", "lockid") + op.drop_column("query_logs", "error_message") + op.drop_column("query_logs", "error_code") + op.drop_column("query_logs", "was_nilrag") + op.drop_column("query_logs", "was_nildb") + op.drop_column("query_logs", "was_multimodal") + op.drop_column("query_logs", "was_streamed") + op.drop_column("query_logs", "tool_response_time_ms") + op.drop_column("query_logs", "model_response_time_ms") + op.drop_column("query_logs", "response_time_ms") + op.drop_column("query_logs", "max_tokens") + op.drop_column("query_logs", "temperature") + op.drop_column("query_logs", "tool_calls") + # ### end revert ### diff --git a/nilai-api/alembic/versions/43b23c73035b_fix_userid_change_to_user_id.py b/nilai-api/alembic/versions/43b23c73035b_fix_userid_change_to_user_id.py new file mode 100644 index 00000000..4c20bb6d --- /dev/null +++ b/nilai-api/alembic/versions/43b23c73035b_fix_userid_change_to_user_id.py @@ -0,0 +1,37 @@ +"""fix: userid change to user_id + +Revision ID: 43b23c73035b +Revises: 0ba073468afc +Create Date: 2025-11-03 11:33:03.006101 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "43b23c73035b" +down_revision: Union[str, None] = "0ba073468afc" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "query_logs", sa.Column("user_id", sa.String(length=75), nullable=False) + ) + op.create_index( + op.f("ix_query_logs_user_id"), "query_logs", ["user_id"], unique=False + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_query_logs_user_id"), table_name="query_logs") + op.drop_column("query_logs", "user_id") + # ### end Alembic commands ### diff --git a/nilai-api/examples/users.py b/nilai-api/examples/users.py deleted file mode 100644 index b6b206d5..00000000 --- a/nilai-api/examples/users.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/python - -from nilai_api.db.logs import QueryLogManager -from nilai_api.db.users import UserManager - - -# Example Usage -async def main(): - # Add some users - bob = await UserManager.insert_user("Bob", "bob@example.com") - alice = await UserManager.insert_user("Alice", "alice@example.com") - - print(f"Bob's details: {bob}") - print(f"Alice's details: {alice}") - - # Check API key - user_name = await UserManager.check_api_key(bob.apikey) - print(f"API key validation: {user_name}") - - # Update and retrieve token usage - await UserManager.update_token_usage( - bob.userid, prompt_tokens=50, completion_tokens=20 - ) - usage = await UserManager.get_user_token_usage(bob.userid) - print(f"Bob's token usage: {usage}") - - # Log a query - await QueryLogManager.log_query( - userid=bob.userid, - model="gpt-3.5-turbo", - prompt_tokens=8, - completion_tokens=7, - web_search_calls=1, - ) - - -if __name__ == "__main__": - import asyncio - from dotenv import load_dotenv - - load_dotenv() - - asyncio.run(main()) diff --git a/nilai-api/pyproject.toml b/nilai-api/pyproject.toml index 053338b8..fd6f1eef 100644 --- a/nilai-api/pyproject.toml +++ b/nilai-api/pyproject.toml @@ -9,7 +9,9 @@ authors = [ ] requires-python = ">=3.12" dependencies = [ + "accelerate>=1.1.1", "alembic>=1.14.1", + "cryptography>=43.0.1", "fastapi[standard]>=0.115.5", "gunicorn>=23.0.0", "nilai-common", @@ -18,24 +20,22 @@ dependencies = [ "uvicorn>=0.32.1", "httpx>=0.27.2", "nilrag>=0.1.11", - "openai>=1.99.2", + "openai>=1.59.9", + "pg8000>=1.31.2", "prometheus_fastapi_instrumentator>=7.0.2", "asyncpg>=0.30.0", - "redis>=6.4.0", + "greenlet>=3.1.1", + "redis>=5.2.1", + "authlib>=1.4.1", + "verifier", "web3>=7.8.0", "click>=8.1.8", "nuc>=0.1.0", "pyyaml>=6.0.1", "trafilatura>=1.7.0", "secretvaults", - "pydantic>=2.0.0", - "ecdsa>=0.19.0", - "secp256k1>=0.14.0", - "hexbytes>=1.2.0", - "eth-account>=0.13.0", - "sentence-transformers>=5.1.1", "e2b-code-interpreter>=1.0.3", - "nilauth-credit-middleware==0.1.1", + "nilauth-credit-middleware>=0.1.2", ] @@ -47,4 +47,4 @@ build-backend = "hatchling.build" nilai-common = { workspace = true } # TODO: Remove this once the secretvaults package is released with the fix -secretvaults = { git = "https://github.com/jcabrero/secretvaults-py", rev = "main" } +secretvaults = { git = "https://github.com/jcabrero/secretvaults-py", rev = "main" } \ No newline at end of file diff --git a/nilai-api/src/nilai_api/attestation/__init__.py b/nilai-api/src/nilai_api/attestation/__init__.py index 795be454..ed13f68e 100644 --- a/nilai-api/src/nilai_api/attestation/__init__.py +++ b/nilai-api/src/nilai_api/attestation/__init__.py @@ -1,34 +1,29 @@ from fastapi import HTTPException import httpx -from nilai_common import Nonce, AttestationReport, SETTINGS -from nilai_common.logger import setup_logger +from nilai_common import AttestationReport -logger = setup_logger(__name__) +ATTESTATION_URL = "http://nilcc-attester/v2/report" -async def get_attestation_report( - nonce: Nonce | None, -) -> AttestationReport: - """Get the attestation report for the given nonce""" +async def get_attestation_report(nonce: str) -> AttestationReport: + """Get the attestation report""" try: - attestation_url = f"http://{SETTINGS.attestation_host}:{SETTINGS.attestation_port}/attestation/report" async with httpx.AsyncClient() as client: - response: httpx.Response = await client.get(attestation_url, params=nonce) - report = AttestationReport(**response.json()) - return report - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -async def verify_attestation_report(attestation_report: AttestationReport) -> bool: - """Verify the attestation report""" - try: - attestation_url = f"http://{SETTINGS.attestation_host}:{SETTINGS.attestation_port}/attestation/verify" - async with httpx.AsyncClient() as client: - response: httpx.Response = await client.get( - attestation_url, params=attestation_report.model_dump() + response: httpx.Response = await client.get(ATTESTATION_URL) + response_json = response.json() + return AttestationReport( + nonce=nonce, + gpu_attestation=response_json["report"], + cpu_attestation=response_json["gpu_token"], + verifying_key="", # Added later by the API ) - return response.json() + except httpx.HTTPStatusError as e: + raise HTTPException( + status_code=e.response.status_code, + detail=str("Error getting attestation report" + str(e)), + ) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException( + status_code=500, detail=str("Error getting attestation report" + str(e)) + ) diff --git a/nilai-api/src/nilai_api/auth/__init__.py b/nilai-api/src/nilai_api/auth/__init__.py index 2e7cd6f7..2123685a 100644 --- a/nilai-api/src/nilai_api/auth/__init__.py +++ b/nilai-api/src/nilai_api/auth/__init__.py @@ -4,7 +4,6 @@ from logging import getLogger from nilai_api.config import CONFIG -from nilai_api.db.users import UserManager from nilai_api.auth.strategies import AuthenticationStrategy from nuc.validate import ValidationException @@ -36,7 +35,6 @@ async def get_auth_info( ) auth_info = await strategy(credentials.credentials) - await UserManager.update_last_activity(userid=auth_info.user.userid) return auth_info except AuthenticationError as e: raise e diff --git a/nilai-api/src/nilai_api/auth/nuc.py b/nilai-api/src/nilai_api/auth/nuc.py index e9f1a9e3..614d9ef1 100644 --- a/nilai-api/src/nilai_api/auth/nuc.py +++ b/nilai-api/src/nilai_api/auth/nuc.py @@ -7,13 +7,13 @@ from nilai_api.state import state from nilai_api.auth.common import AuthenticationError -from nilai_common.logger import setup_logger from nilai_api.auth.nuc_helpers.usage import TokenRateLimits from nilai_api.auth.nuc_helpers.nildb_document import PromptDocument -logger = setup_logger(__name__) +import logging +logger = logging.getLogger(__name__) NILAI_BASE_COMMAND: Command = Command.parse("/nil/ai") @@ -86,11 +86,11 @@ def validate_nuc(nuc_token: str) -> Tuple[str, str]: # Validate the # Return the subject of the token, the subscription holder - subscription_holder = token.subject.public_key.hex() - user = token.issuer.public_key.hex() + subscription_holder = token.subject + user = token.issuer logger.info(f"Subscription holder: {subscription_holder}") logger.info(f"User: {user}") - return subscription_holder, user + return str(subscription_holder), str(user) def get_token_rate_limit(nuc_token: str) -> Optional[TokenRateLimits]: diff --git a/nilai-api/src/nilai_api/auth/strategies.py b/nilai-api/src/nilai_api/auth/strategies.py index 9917ee39..089e7e94 100644 --- a/nilai-api/src/nilai_api/auth/strategies.py +++ b/nilai-api/src/nilai_api/auth/strategies.py @@ -1,6 +1,6 @@ from typing import Callable, Awaitable, Optional -from datetime import datetime, timezone +from fastapi import HTTPException from nilai_api.db.users import UserManager, UserModel, UserData from nilai_api.auth.nuc import ( validate_nuc, @@ -11,11 +11,18 @@ from nilai_api.auth.common import ( PromptDocument, TokenRateLimits, - AuthenticationInfo, AuthenticationError, + AuthenticationInfo, +) + +from nilauth_credit_middleware import ( + CreditClientSingleton, ) +from nilauth_credit_middleware.api_model import ValidateCredentialResponse + from enum import Enum + # All strategies must return a UserModel # The strategies can raise any exception, which will be caught and converted to an AuthenticationError # The exception detail will be passed to the client @@ -44,18 +51,10 @@ async def wrapper(token) -> AuthenticationInfo: return await function(token) if token == allowed_token: - user_model: UserModel | None = await UserManager.check_user( - allowed_token + user_model = UserModel( + user_id=allowed_token, + rate_limits=None, ) - if user_model is None: - user_model = UserModel( - userid=allowed_token, - name=allowed_token, - apikey=allowed_token, - signup_date=datetime.now(timezone.utc), - ) - await UserManager.insert_user_model(user_model) - return AuthenticationInfo( user=UserData.from_sqlalchemy(user_model), token_rate_limit=None, @@ -68,16 +67,41 @@ async def wrapper(token) -> AuthenticationInfo: return decorator +async def validate_credential(credential: str, is_public: bool) -> UserModel: + """ + Validate a credential with nilauth credit middleware and return the user model + """ + credit_client = CreditClientSingleton.get_client() + try: + validate_response: ValidateCredentialResponse = ( + await credit_client.validate_credential(credential, is_public=is_public) + ) + except HTTPException as e: + if e.status_code == 404: + raise AuthenticationError(f"Credential not found: {e.detail}") + elif e.status_code == 401: + raise AuthenticationError(f"Credential is inactive: {e.detail}") + else: + raise AuthenticationError(f"Failed to validate credential: {e.detail}") + + user_model = await UserManager.check_user(validate_response.user_id) + if user_model is None: + user_model = UserModel( + user_id=validate_response.user_id, + rate_limits=None, + ) + return user_model + + @allow_token(CONFIG.docs.token) async def api_key_strategy(api_key: str) -> AuthenticationInfo: - user_model: Optional[UserModel] = await UserManager.check_api_key(api_key) - if user_model: - return AuthenticationInfo( - user=UserData.from_sqlalchemy(user_model), - token_rate_limit=None, - prompt_document=None, - ) - raise AuthenticationError("Missing or invalid API key") + user_model = await validate_credential(api_key, is_public=False) + + return AuthenticationInfo( + user=UserData.from_sqlalchemy(user_model), + token_rate_limit=None, + prompt_document=None, + ) @allow_token(CONFIG.docs.token) @@ -89,20 +113,7 @@ async def nuc_strategy(nuc_token) -> AuthenticationInfo: token_rate_limits: Optional[TokenRateLimits] = get_token_rate_limit(nuc_token) prompt_document: Optional[PromptDocument] = get_token_prompt_document(nuc_token) - user_model: Optional[UserModel] = await UserManager.check_user(user) - if user_model: - return AuthenticationInfo( - user=UserData.from_sqlalchemy(user_model), - token_rate_limit=token_rate_limits, - prompt_document=prompt_document, - ) - - user_model = UserModel( - userid=user, - name=user, - apikey=subscription_holder, - ) - await UserManager.insert_user_model(user_model) + user_model = await validate_credential(subscription_holder, is_public=True) return AuthenticationInfo( user=UserData.from_sqlalchemy(user_model), token_rate_limit=token_rate_limits, diff --git a/nilai-api/src/nilai_api/commands/add_user.py b/nilai-api/src/nilai_api/commands/add_user.py index e9f49e55..202b70d4 100644 --- a/nilai-api/src/nilai_api/commands/add_user.py +++ b/nilai-api/src/nilai_api/commands/add_user.py @@ -6,9 +6,7 @@ @click.command() -@click.option("--name", type=str, required=True, help="User Name") -@click.option("--apikey", type=str, help="API Key") -@click.option("--userid", type=str, help="User Id") +@click.option("--user_id", type=str, help="User Id") @click.option("--ratelimit-day", type=int, help="number of request per day") @click.option("--ratelimit-hour", type=int, help="number of request per hour") @click.option("--ratelimit-minute", type=int, help="number of request per minute") @@ -26,9 +24,7 @@ help="number of web search request per minute", ) def main( - name, - apikey: str | None, - userid: str | None, + user_id: str | None, ratelimit_day: int | None, ratelimit_hour: int | None, ratelimit_minute: int | None, @@ -38,9 +34,7 @@ def main( ): async def add_user(): user: UserModel = await UserManager.insert_user( - name, - apikey, - userid, + user_id, RateLimits( user_rate_limit_day=ratelimit_day, user_rate_limit_hour=ratelimit_hour, @@ -52,9 +46,7 @@ async def add_user(): ) json_user = json.dumps( { - "userid": user.userid, - "name": user.name, - "apikey": user.apikey, + "user_id": user.user_id, "ratelimit_day": user.rate_limits_obj.user_rate_limit_day, "ratelimit_hour": user.rate_limits_obj.user_rate_limit_hour, "ratelimit_minute": user.rate_limits_obj.user_rate_limit_minute, diff --git a/nilai-api/src/nilai_api/config/__init__.py b/nilai-api/src/nilai_api/config/__init__.py index 59f1d601..3f19f85e 100644 --- a/nilai-api/src/nilai_api/config/__init__.py +++ b/nilai-api/src/nilai_api/config/__init__.py @@ -3,7 +3,7 @@ import logging from pydantic import BaseModel from .environment import EnvironmentConfig -from .database import DatabaseConfig, EtcdConfig, RedisConfig +from .database import DatabaseConfig, DiscoveryConfig, RedisConfig from .auth import AuthConfig, DocsConfig from .nildb import NilDBConfig from .web_search import WebSearchSettings @@ -20,7 +20,9 @@ class NilAIConfig(BaseModel): database: DatabaseConfig = create_config_model( DatabaseConfig, "database", CONFIG_DATA, "POSTGRES_" ) - etcd: EtcdConfig = create_config_model(EtcdConfig, "etcd", CONFIG_DATA, "ETCD_") + discovery: DiscoveryConfig = create_config_model( + DiscoveryConfig, "discovery", CONFIG_DATA, "DISCOVERY_" + ) redis: RedisConfig = create_config_model( RedisConfig, "redis", CONFIG_DATA, "REDIS_" ) @@ -68,3 +70,4 @@ def prettify(self): ] logging.info(CONFIG.prettify()) +print(CONFIG.prettify()) diff --git a/nilai-api/src/nilai_api/config/database.py b/nilai-api/src/nilai_api/config/database.py index 6cc1371a..31c8aa06 100644 --- a/nilai-api/src/nilai_api/config/database.py +++ b/nilai-api/src/nilai_api/config/database.py @@ -9,10 +9,10 @@ class DatabaseConfig(BaseModel): db: str = Field(description="Database name") -class EtcdConfig(BaseModel): - host: str = Field(description="ETCD host") - port: int = Field(description="ETCD port") +class DiscoveryConfig(BaseModel): + host: str = Field(default="localhost", description="Redis host for discovery") + port: int = Field(default=6379, description="Redis port for discovery") class RedisConfig(BaseModel): - url: str = Field(description="Redis URL") + url: str = Field(description="Redis URL for rate limiting") diff --git a/nilai-api/src/nilai_api/credit.py b/nilai-api/src/nilai_api/credit.py index 46f5dcce..b9d7ea6f 100644 --- a/nilai-api/src/nilai_api/credit.py +++ b/nilai-api/src/nilai_api/credit.py @@ -17,6 +17,17 @@ logger = logging.getLogger(__name__) +class NoOpMeteringContext: + """A no-op metering context for requests that should skip metering (e.g., Docs Token).""" + + def __init__(self): + self.lock_id: str = "noop-lock-id" + + def set_response(self, response_data: dict) -> None: + """No-op method that does nothing.""" + pass + + class LLMCost(BaseModel): prompt_tokens_price: float completion_tokens_price: float @@ -92,7 +103,7 @@ class LLMResponse(BaseModel): ) -def user_id_extractor() -> Callable[[Request], Awaitable[str]]: +def credential_extractor() -> Callable[[Request], Awaitable[str]]: if CONFIG.auth.auth_strategy == "nuc": return from_nuc_bearer_root_token() else: @@ -144,8 +155,35 @@ async def calculator(request: Request, response_data: dict) -> float: return calculator -LLMMeter = create_metering_dependency( - user_id_extractor=user_id_extractor(), +_base_llm_meter = create_metering_dependency( + credential_extractor=credential_extractor(), estimated_cost=2.0, cost_calculator=llm_cost_calculator(MyCostDictionary), + public_identifiers=CONFIG.auth.auth_strategy == "nuc", ) + + +async def LLMMeter(request: Request): + """ + Metering dependency that skips metering for Docs Token requests. + """ + # Check if the request is using the docs token + if CONFIG.docs.token: + auth_header: str | None = request.headers.get("Authorization", None) + if auth_header: + # Extract the token from the Bearer header + token = ( + auth_header.replace("Bearer ", "") + if auth_header.startswith("Bearer ") + else auth_header + ) + + # Skip metering if this is the docs token + if token == CONFIG.docs.token: + logger.info("Skipping metering for Docs Token request") + yield NoOpMeteringContext() + return + + # Otherwise, apply normal metering + async for meter in _base_llm_meter(request): + yield meter diff --git a/nilai-api/src/nilai_api/db/__init__.py b/nilai-api/src/nilai_api/db/__init__.py index ee70ffe0..0cc8a623 100644 --- a/nilai-api/src/nilai_api/db/__init__.py +++ b/nilai-api/src/nilai_api/db/__init__.py @@ -14,11 +14,11 @@ from nilai_api.config import CONFIG -_engine: Optional[sqlalchemy.ext.asyncio.AsyncEngine] = None +_engine: Optional[sqlalchemy.ext.asyncio.AsyncEngine] = None # type: ignore[reportAttributeAccessIssue] _SessionLocal: Optional[sessionmaker] = None # Create base and engine with improved configuration -Base = sqlalchemy.orm.declarative_base() +Base = sqlalchemy.orm.declarative_base() # type: ignore[reportAttributeAccessIssue] logger = logging.getLogger(__name__) @@ -49,7 +49,7 @@ def from_env() -> "DatabaseConfig": return DatabaseConfig(database_url) -def get_engine() -> sqlalchemy.ext.asyncio.AsyncEngine: +def get_engine() -> sqlalchemy.ext.asyncio.AsyncEngine: # type: ignore[reportAttributeAccessIssue] global _engine if _engine is None: config = DatabaseConfig.from_env() diff --git a/nilai-api/src/nilai_api/db/logs.py b/nilai-api/src/nilai_api/db/logs.py index 030c8696..4a78c8a7 100644 --- a/nilai-api/src/nilai_api/db/logs.py +++ b/nilai-api/src/nilai_api/db/logs.py @@ -1,12 +1,14 @@ import logging +import time from datetime import datetime, timezone +from typing import Optional +from nilai_common import Usage import sqlalchemy -from sqlalchemy import ForeignKey, Integer, String, DateTime, Text +from sqlalchemy import Integer, String, DateTime, Text, Boolean, Float from sqlalchemy.exc import SQLAlchemyError from nilai_api.db import Base, Column, get_db_session -from nilai_api.db.users import UserModel logger = logging.getLogger(__name__) @@ -16,9 +18,8 @@ class QueryLog(Base): __tablename__ = "query_logs" id: int = Column(Integer, primary_key=True, autoincrement=True) # type: ignore - userid: str = Column( - String(75), ForeignKey(UserModel.userid), nullable=False, index=True - ) # type: ignore + user_id: str = Column(String(75), nullable=False, index=True) # type: ignore + lockid: str = Column(String(75), nullable=False, index=True) # type: ignore query_timestamp: datetime = Column( DateTime(timezone=True), server_default=sqlalchemy.func.now(), nullable=False ) # type: ignore @@ -26,51 +27,285 @@ class QueryLog(Base): prompt_tokens: int = Column(Integer, nullable=False) # type: ignore completion_tokens: int = Column(Integer, nullable=False) # type: ignore total_tokens: int = Column(Integer, nullable=False) # type: ignore + tool_calls: int = Column(Integer, nullable=False) # type: ignore web_search_calls: int = Column(Integer, nullable=False) # type: ignore + temperature: Optional[float] = Column(Float, nullable=True) # type: ignore + max_tokens: Optional[int] = Column(Integer, nullable=True) # type: ignore + + response_time_ms: int = Column(Integer, nullable=False) # type: ignore + model_response_time_ms: int = Column(Integer, nullable=False) # type: ignore + tool_response_time_ms: int = Column(Integer, nullable=False) # type: ignore + + was_streamed: bool = Column(Boolean, nullable=False) # type: ignore + was_multimodal: bool = Column(Boolean, nullable=False) # type: ignore + was_nildb: bool = Column(Boolean, nullable=False) # type: ignore + was_nilrag: bool = Column(Boolean, nullable=False) # type: ignore + + error_code: int = Column(Integer, nullable=False) # type: ignore + error_message: str = Column(Text, nullable=False) # type: ignore def __repr__(self): - return f"" + return f"" + + +class QueryLogContext: + """ + Context manager for logging query metrics during a request. + Used as a FastAPI dependency to track request metrics. + """ + + def __init__(self): + self.user_id: Optional[str] = None + self.lockid: Optional[str] = None + self.model: Optional[str] = None + self.prompt_tokens: int = 0 + self.completion_tokens: int = 0 + self.tool_calls: int = 0 + self.web_search_calls: int = 0 + self.temperature: Optional[float] = None + self.max_tokens: Optional[int] = None + self.was_streamed: bool = False + self.was_multimodal: bool = False + self.was_nildb: bool = False + self.was_nilrag: bool = False + self.error_code: int = 0 + self.error_message: str = "" + + # Timing tracking + self.start_time: float = time.monotonic() + self.model_start_time: Optional[float] = None + self.model_end_time: Optional[float] = None + self.tool_start_time: Optional[float] = None + self.tool_end_time: Optional[float] = None + + def set_user(self, user_id: str) -> None: + """Set the user ID for this query.""" + self.user_id = user_id + + def set_lockid(self, lockid: str) -> None: + """Set the lock ID for this query.""" + self.lockid = lockid + + def set_model(self, model: str) -> None: + """Set the model name for this query.""" + self.model = model + + def set_request_params( + self, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + was_streamed: bool = False, + was_multimodal: bool = False, + was_nildb: bool = False, + was_nilrag: bool = False, + ) -> None: + """Set request parameters.""" + self.temperature = temperature + self.max_tokens = max_tokens + self.was_streamed = was_streamed + self.was_multimodal = was_multimodal + self.was_nildb = was_nildb + self.was_nilrag = was_nilrag + + def set_usage( + self, + prompt_tokens: int = 0, + completion_tokens: int = 0, + tool_calls: int = 0, + web_search_calls: int = 0, + ) -> None: + """Set token usage and feature usage.""" + self.prompt_tokens = prompt_tokens + self.completion_tokens = completion_tokens + self.tool_calls = tool_calls + self.web_search_calls = web_search_calls + + def set_error(self, error_code: int, error_message: str) -> None: + """Set error information.""" + self.error_code = error_code + self.error_message = error_message + + def start_model_timing(self) -> None: + """Mark the start of model inference.""" + self.model_start_time = time.monotonic() + + def end_model_timing(self) -> None: + """Mark the end of model inference.""" + self.model_end_time = time.monotonic() + + def start_tool_timing(self) -> None: + """Mark the start of tool execution.""" + self.tool_start_time = time.monotonic() + + def end_tool_timing(self) -> None: + """Mark the end of tool execution.""" + self.tool_end_time = time.monotonic() + + def _calculate_timings(self) -> tuple[int, int, int]: + """Calculate response times in milliseconds.""" + total_ms = int((time.monotonic() - self.start_time) * 1000) + + model_ms = 0 + if self.model_start_time and self.model_end_time: + model_ms = int((self.model_end_time - self.model_start_time) * 1000) + + tool_ms = 0 + if self.tool_start_time and self.tool_end_time: + tool_ms = int((self.tool_end_time - self.tool_start_time) * 1000) + + return total_ms, model_ms, tool_ms + + async def commit(self) -> None: + """ + Commit the query log to the database. + Should be called at the end of the request lifecycle. + """ + if not self.user_id or not self.model: + logger.warning( + "Skipping query log: user_id or model not set " + f"(user_id={self.user_id}, model={self.model})" + ) + return + + total_ms, model_ms, tool_ms = self._calculate_timings() + total_tokens = self.prompt_tokens + self.completion_tokens + + try: + async with get_db_session() as session: + query_log = QueryLog( + user_id=self.user_id, + lockid=self.lockid, + model=self.model, + prompt_tokens=self.prompt_tokens, + completion_tokens=self.completion_tokens, + total_tokens=total_tokens, + tool_calls=self.tool_calls, + web_search_calls=self.web_search_calls, + temperature=self.temperature, + max_tokens=self.max_tokens, + query_timestamp=datetime.now(timezone.utc), + response_time_ms=total_ms, + model_response_time_ms=model_ms, + tool_response_time_ms=tool_ms, + was_streamed=self.was_streamed, + was_multimodal=self.was_multimodal, + was_nilrag=self.was_nilrag, + was_nildb=self.was_nildb, + error_code=self.error_code, + error_message=self.error_message, + ) + session.add(query_log) + await session.commit() + logger.info( + f"Query logged for user {self.user_id}: model={self.model}, " + f"tokens={total_tokens}, total_ms={total_ms}" + ) + except SQLAlchemyError as e: + logger.error(f"Error logging query: {e}") + # Don't raise - logging failure shouldn't break the request class QueryLogManager: + """Static methods for direct query logging (legacy support).""" + @staticmethod async def log_query( - userid: str, + user_id: str, + lockid: str, model: str, prompt_tokens: int, completion_tokens: int, + response_time_ms: int, web_search_calls: int, + was_streamed: bool, + was_multimodal: bool, + was_nilrag: bool, + was_nildb: bool, + tool_calls: int = 0, + temperature: float = 1.0, + max_tokens: int = 0, + model_response_time_ms: int = 0, + tool_response_time_ms: int = 0, + error_code: int = 0, + error_message: str = "", ): """ - Log a user's query. - - Args: - userid (str): User's unique ID - model (str): The model that generated the response - prompt_tokens (int): Number of input tokens used - completion_tokens (int): Number of tokens in the generated response + Log a user's query (legacy method). + Consider using QueryLogContext as a dependency instead. """ total_tokens = prompt_tokens + completion_tokens try: async with get_db_session() as session: query_log = QueryLog( - userid=userid, + user_id=user_id, + lockid=lockid, model=model, prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, total_tokens=total_tokens, - query_timestamp=datetime.now(timezone.utc), + tool_calls=tool_calls, web_search_calls=web_search_calls, + temperature=temperature, + max_tokens=max_tokens, + query_timestamp=datetime.now(timezone.utc), + response_time_ms=response_time_ms, + model_response_time_ms=model_response_time_ms, + tool_response_time_ms=tool_response_time_ms, + was_streamed=was_streamed, + was_multimodal=was_multimodal, + was_nilrag=was_nilrag, + was_nildb=was_nildb, + error_code=error_code, + error_message=error_message, ) session.add(query_log) await session.commit() logger.info( - f"Query logged for user {userid} with total tokens {total_tokens}." + f"Query logged for user {user_id} with total tokens {total_tokens}." ) except SQLAlchemyError as e: logger.error(f"Error logging query: {e}") raise + @staticmethod + async def get_user_token_usage(user_id: str) -> Optional[Usage]: + """ + Get aggregated token usage for a specific user using server-side SQL aggregation. + This is more efficient than fetching all records and calculating in Python. + """ + try: + async with get_db_session() as session: + # Use SQL aggregation functions to calculate on the database server + query = ( + sqlalchemy.select( + sqlalchemy.func.coalesce( + sqlalchemy.func.sum(QueryLog.prompt_tokens), 0 + ).label("prompt_tokens"), + sqlalchemy.func.coalesce( + sqlalchemy.func.sum(QueryLog.completion_tokens), 0 + ).label("completion_tokens"), + sqlalchemy.func.coalesce( + sqlalchemy.func.sum(QueryLog.total_tokens), 0 + ).label("total_tokens"), + sqlalchemy.func.count().label("queries"), + ).where(QueryLog.user_id == user_id) # type: ignore[arg-type] + ) + + result = await session.execute(query) + row = result.one_or_none() + + if row is None: + return None + + return Usage( + prompt_tokens=int(row.prompt_tokens), + completion_tokens=int(row.completion_tokens), + total_tokens=int(row.total_tokens), + ) + except SQLAlchemyError as e: + logger.error(f"Error getting token usage: {e}") + return None + -__all__ = ["QueryLogManager", "QueryLog"] +__all__ = ["QueryLogManager", "QueryLog", "QueryLogContext"] diff --git a/nilai-api/src/nilai_api/db/users.py b/nilai-api/src/nilai_api/db/users.py index 515ba389..e475c424 100644 --- a/nilai-api/src/nilai_api/db/users.py +++ b/nilai-api/src/nilai_api/db/users.py @@ -2,11 +2,10 @@ import uuid from pydantic import BaseModel, ConfigDict, Field -from datetime import datetime, timezone -from typing import Any, Dict, List, Optional +from typing import Optional import sqlalchemy -from sqlalchemy import Integer, String, DateTime, JSON +from sqlalchemy import String, JSON from sqlalchemy.exc import SQLAlchemyError from nilai_api.db import Base, Column, get_db_session @@ -57,21 +56,11 @@ def get_effective_limits(self) -> "RateLimits": # Enhanced User Model with additional constraints and validation class UserModel(Base): __tablename__ = "users" - - userid: str = Column(String(75), primary_key=True, index=True) # type: ignore - name: str = Column(String(100), nullable=False) # type: ignore - apikey: str = Column(String(75), unique=False, nullable=False, index=True) # type: ignore - prompt_tokens: int = Column(Integer, default=0, nullable=False) # type: ignore - completion_tokens: int = Column(Integer, default=0, nullable=False) # type: ignore - queries: int = Column(Integer, default=0, nullable=False) # type: ignore - signup_date: datetime = Column( - DateTime(timezone=True), server_default=sqlalchemy.func.now(), nullable=False - ) # type: ignore - last_activity: datetime = Column(DateTime(timezone=True), nullable=True) # type: ignore + user_id: str = Column(String(75), primary_key=True, index=True) # type: ignore rate_limits: dict = Column(JSON, nullable=True) # type: ignore def __repr__(self): - return f"" + return f"" @property def rate_limits_obj(self) -> RateLimits: @@ -85,14 +74,7 @@ def to_pydantic(self) -> "UserData": class UserData(BaseModel): - userid: str - name: str - apikey: str - prompt_tokens: int = 0 - completion_tokens: int = 0 - queries: int = 0 - signup_date: datetime - last_activity: Optional[datetime] = None + user_id: str # apikey or subscription holder public key rate_limits: RateLimits = Field(default_factory=RateLimits().get_effective_limits) model_config = ConfigDict(from_attributes=True) @@ -100,21 +82,10 @@ class UserData(BaseModel): @classmethod def from_sqlalchemy(cls, user: UserModel) -> "UserData": return cls( - userid=user.userid, - name=user.name, - apikey=user.apikey, - prompt_tokens=user.prompt_tokens or 0, - completion_tokens=user.completion_tokens or 0, - queries=user.queries or 0, - signup_date=user.signup_date or datetime.now(timezone.utc), - last_activity=user.last_activity, + user_id=user.user_id, rate_limits=user.rate_limits_obj, ) - @property - def is_subscription_owner(self): - return self.userid == self.apikey - class UserManager: @staticmethod @@ -127,31 +98,9 @@ def generate_api_key() -> str: """Generate a unique API key.""" return str(uuid.uuid4()) - @staticmethod - async def update_last_activity(userid: str): - """ - Update the last activity timestamp for a user. - - Args: - userid (str): User's unique ID - """ - try: - async with get_db_session() as session: - user = await session.get(UserModel, userid) - if user: - user.last_activity = datetime.now(timezone.utc) - await session.commit() - logger.info(f"Updated last activity for user {userid}") - else: - logger.warning(f"User {userid} not found") - except SQLAlchemyError as e: - logger.error(f"Error updating last activity: {e}") - @staticmethod async def insert_user( - name: str, - apikey: str | None = None, - userid: str | None = None, + user_id: str | None = None, rate_limits: RateLimits | None = None, ) -> UserModel: """ @@ -160,19 +109,16 @@ async def insert_user( Args: name (str): Name of the user apikey (str): API key for the user - userid (str): Unique ID for the user + user_id (str): Unique ID for the user rate_limits (RateLimits): Rate limit configuration Returns: UserModel: The created user model """ - userid = userid if userid else UserManager.generate_user_id() - apikey = apikey if apikey else UserManager.generate_api_key() + user_id = user_id if user_id else UserManager.generate_user_id() user = UserModel( - userid=userid, - name=name, - apikey=apikey, + user_id=user_id, rate_limits=rate_limits.model_dump() if rate_limits else None, ) return await UserManager.insert_user_model(user) @@ -189,35 +135,14 @@ async def insert_user_model(user: UserModel) -> UserModel: async with get_db_session() as session: session.add(user) await session.commit() - logger.info(f"User {user.name} added successfully.") + logger.info(f"User {user.user_id} added successfully.") return user except SQLAlchemyError as e: logger.error(f"Error inserting user: {e}") raise @staticmethod - async def check_user(userid: str) -> Optional[UserModel]: - """ - Validate a user. - - Args: - userid (str): User ID to validate - - Returns: - User's name if user is valid, None otherwise - """ - try: - async with get_db_session() as session: - query = sqlalchemy.select(UserModel).filter(UserModel.userid == userid) # type: ignore - user = await session.execute(query) - user = user.scalar_one_or_none() - return user - except SQLAlchemyError as e: - logger.error(f"Error checking API key: {e}") - return None - - @staticmethod - async def check_api_key(api_key: str) -> Optional[UserModel]: + async def check_user(user_id: str) -> Optional[UserModel]: """ Validate an API key. @@ -225,118 +150,27 @@ async def check_api_key(api_key: str) -> Optional[UserModel]: api_key (str): API key to validate Returns: - User's name if API key is valid, None otherwise + User's rate limits if user id is valid, None otherwise """ try: async with get_db_session() as session: - query = sqlalchemy.select(UserModel).filter(UserModel.apikey == api_key) # type: ignore + query = sqlalchemy.select(UserModel).filter( + UserModel.user_id == user_id # type: ignore + ) user = await session.execute(query) user = user.scalar_one_or_none() return user except SQLAlchemyError as e: - logger.error(f"Error checking API key: {e}") - return None - - @staticmethod - async def update_token_usage( - userid: str, prompt_tokens: int, completion_tokens: int - ): - """ - Update token usage for a specific user. - - Args: - userid (str): User's unique ID - prompt_tokens (int): Number of input tokens - completion_tokens (int): Number of generated tokens - """ - try: - async with get_db_session() as session: - user = await session.get(UserModel, userid) - if user: - user.prompt_tokens += prompt_tokens - user.completion_tokens += completion_tokens - user.queries += 1 - await session.commit() - logger.info(f"Updated token usage for user {userid}") - else: - logger.warning(f"User {userid} not found") - except SQLAlchemyError as e: - logger.error(f"Error updating token usage: {e}") - - @staticmethod - async def get_token_usage(userid: str) -> Optional[Dict[str, Any]]: - """ - Get token usage for a specific user. - - Args: - userid (str): User's unique ID - """ - try: - async with get_db_session() as session: - user = await session.get(UserModel, userid) - if user: - return { - "prompt_tokens": user.prompt_tokens, - "completion_tokens": user.completion_tokens, - "total_tokens": user.prompt_tokens + user.completion_tokens, - "queries": user.queries, - } - else: - logger.warning(f"User {userid} not found") - return None - except SQLAlchemyError as e: - logger.error(f"Error updating token usage: {e}") - return None - - @staticmethod - async def get_all_users() -> Optional[List[UserData]]: - """ - Retrieve all users from the database. - - Returns: - List of UserData or None if no users found - """ - try: - async with get_db_session() as session: - users = await session.execute(sqlalchemy.select(UserModel)) - users = users.scalars().all() - return [UserData.from_sqlalchemy(user) for user in users] - except SQLAlchemyError as e: - logger.error(f"Error retrieving all users: {e}") - return None - - @staticmethod - async def get_user_token_usage(userid: str) -> Optional[Dict[str, int]]: - """ - Retrieve total token usage for a user. - - Args: - userid (str): User's unique ID - - Returns: - Dict of token usage or None if user not found - """ - try: - async with get_db_session() as session: - user = await session.get(UserModel, userid) - if user: - return { - "prompt_tokens": user.prompt_tokens, - "completion_tokens": user.completion_tokens, - "queries": user.queries, - } - return None - except SQLAlchemyError as e: - logger.error(f"Error retrieving token usage: {e}") + logger.error(f"Rate limit checking user id: {e}") return None @staticmethod - async def update_rate_limits(userid: str, rate_limits: RateLimits) -> bool: + async def update_rate_limits(user_id: str, rate_limits: RateLimits) -> bool: """ Update rate limits for a specific user. Args: - userid (str): User's unique ID + user_id (str): User's unique ID rate_limits (RateLimits): New rate limit configuration Returns: @@ -344,14 +178,14 @@ async def update_rate_limits(userid: str, rate_limits: RateLimits) -> bool: """ try: async with get_db_session() as session: - user = await session.get(UserModel, userid) + user = await session.get(UserModel, user_id) if user: user.rate_limits = rate_limits.model_dump() await session.commit() - logger.info(f"Updated rate limits for user {userid}") + logger.info(f"Updated rate limits for user {user_id}") return True else: - logger.warning(f"User {userid} not found") + logger.warning(f"User {user_id} not found") return False except SQLAlchemyError as e: logger.error(f"Error updating rate limits: {e}") diff --git a/nilai-api/src/nilai_api/handlers/nildb/handler.py b/nilai-api/src/nilai_api/handlers/nildb/handler.py index 80d06182..5e75766d 100644 --- a/nilai-api/src/nilai_api/handlers/nildb/handler.py +++ b/nilai-api/src/nilai_api/handlers/nildb/handler.py @@ -96,11 +96,9 @@ async def get_nildb_delegation_token(user_did: str) -> PromptDelegationToken: return PromptDelegationToken(token=delegation_token, did=builder_did) -""" Read nilDB records from owned data collection based on the store id given by the user on the request """ - - async def get_prompt_from_nildb(prompt_document: PromptDocument) -> str: """Read a specific document - core functionality""" + read_params = ReadDataRequestParams( collection=CONFIG.nildb.collection, document=Uuid(prompt_document.document_id), @@ -125,7 +123,6 @@ async def get_prompt_from_nildb(prompt_document: PromptDocument) -> str: data_dict = document_data.model_dump() else: data_dict = dict(document_data) if document_data else {} - if data_dict.get("owner", None) != str(prompt_document.owner_did): raise ValueError( "Non-owning entity trying to invoke access to a document resource" diff --git a/nilai-api/src/nilai_api/rate_limiting.py b/nilai-api/src/nilai_api/rate_limiting.py index c2d03273..0a70f15f 100644 --- a/nilai-api/src/nilai_api/rate_limiting.py +++ b/nilai-api/src/nilai_api/rate_limiting.py @@ -1,3 +1,4 @@ +import logging from asyncio import iscoroutine from typing import Callable, Tuple, Awaitable, Annotated @@ -11,6 +12,8 @@ from nilai_api.auth import get_auth_info, AuthenticationInfo, TokenRateLimits from nilai_api.config import CONFIG +logger = logging.getLogger(__name__) + LUA_RATE_LIMIT_SCRIPT = """ local key = KEYS[1] local limit = tonumber(ARGV[1]) @@ -52,7 +55,7 @@ async def _extract_coroutine_result(maybe_future, request: Request): class UserRateLimits(BaseModel): - subscription_holder: str + user_id: str token_rate_limit: TokenRateLimits | None rate_limits: RateLimits @@ -60,14 +63,13 @@ class UserRateLimits(BaseModel): def get_user_limits( auth_info: Annotated[AuthenticationInfo, Depends(get_auth_info)], ) -> UserRateLimits: - # TODO: When the only allowed strategy is NUC, we can change the apikey name to subscription_holder - # In apikey mode, the apikey is unique as the userid. - # In nuc mode, the apikey is associated with a subscription holder and the userid is the user + # In apikey mode, the apikey is unique as the user_id. + # In nuc mode, the apikey is associated with a subscription holder and the user_id is the user # For NUCs we want the rate limit to be per subscription holder, not per user - # In JWT mode, the apikey is the userid too + # In JWT mode, the apikey is the user_id too # So we use the apikey as the id return UserRateLimits( - subscription_holder=auth_info.user.apikey, + user_id=auth_info.user.user_id, token_rate_limit=auth_info.token_rate_limit, rate_limits=auth_info.user.rate_limits, ) @@ -105,21 +107,21 @@ async def __call__( await self.check_bucket( redis, redis_rate_limit_command, - f"minute:{user_limits.subscription_holder}", + f"minute:{user_limits.user_id}", user_limits.rate_limits.user_rate_limit_minute, MINUTE_MS, ) await self.check_bucket( redis, redis_rate_limit_command, - f"hour:{user_limits.subscription_holder}", + f"hour:{user_limits.user_id}", user_limits.rate_limits.user_rate_limit_hour, HOUR_MS, ) await self.check_bucket( redis, redis_rate_limit_command, - f"day:{user_limits.subscription_holder}", + f"day:{user_limits.user_id}", user_limits.rate_limits.user_rate_limit_day, DAY_MS, ) @@ -127,7 +129,7 @@ async def __call__( await self.check_bucket( redis, redis_rate_limit_command, - f"user:{user_limits.subscription_holder}", + f"user:{user_limits.user_id}", user_limits.rate_limits.user_rate_limit, 0, # No expiration for for-good rate limit ) @@ -159,21 +161,21 @@ async def __call__( await self.check_bucket( redis, redis_rate_limit_command, - f"web_search_minute:{user_limits.subscription_holder}", + f"web_search_minute:{user_limits.user_id}", user_limits.rate_limits.web_search_rate_limit_minute, MINUTE_MS, ) await self.check_bucket( redis, redis_rate_limit_command, - f"web_search_hour:{user_limits.subscription_holder}", + f"web_search_hour:{user_limits.user_id}", user_limits.rate_limits.web_search_rate_limit_hour, HOUR_MS, ) await self.check_bucket( redis, redis_rate_limit_command, - f"web_search_day:{user_limits.subscription_holder}", + f"web_search_day:{user_limits.user_id}", user_limits.rate_limits.web_search_rate_limit_day, DAY_MS, ) @@ -187,7 +189,7 @@ async def __call__( await self.check_bucket( redis, redis_rate_limit_command, - f"web_search:{user_limits.subscription_holder}", + f"web_search:{user_limits.user_id}", user_limits.rate_limits.web_search_rate_limit, 0, ) @@ -206,13 +208,33 @@ async def check_bucket( times: int | None, milliseconds: int, ): + """ + Check if the rate limit is exceeded for a given key + + Args: + redis: The Redis client + redis_rate_limit_command: The Redis rate limit command + key: The key to check the rate limit for + times: The number of times allowed for the key + milliseconds: The expiration time in milliseconds of the rate limit + + Returns: + None if the rate limit is not exceeded + The number of milliseconds to wait before the rate limit is reset if the rate limit is exceeded + + Raises: + HTTPException: If the rate limit is exceeded + """ if times is None: return + # Evaluate the Lua script to check if the rate limit is exceeded expire = await redis.evalsha( redis_rate_limit_command, 1, key, str(times), str(milliseconds) ) # type: ignore - if int(expire) > 0: + logger.error( + f"Rate limit exceeded for key: {key}, expires in: {expire} milliseconds, times allowed: {times}, expiration time: {milliseconds / 1000} seconds" + ) raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too Many Requests", diff --git a/nilai-api/src/nilai_api/routers/endpoints/chat.py b/nilai-api/src/nilai_api/routers/endpoints/chat.py index 45425e31..a72c9594 100644 --- a/nilai-api/src/nilai_api/routers/endpoints/chat.py +++ b/nilai-api/src/nilai_api/routers/endpoints/chat.py @@ -5,7 +5,15 @@ from base64 import b64encode from typing import AsyncGenerator, Optional, Union, List, Tuple -from fastapi import APIRouter, Body, Depends, HTTPException, status, Request +from fastapi import ( + APIRouter, + Body, + Depends, + HTTPException, + status, + Request, + BackgroundTasks, +) from fastapi.responses import StreamingResponse from openai import AsyncOpenAI @@ -13,8 +21,7 @@ from nilai_api.config import CONFIG from nilai_api.crypto import sign_message from nilai_api.credit import LLMMeter, LLMUsage -from nilai_api.db.logs import QueryLogManager -from nilai_api.db.users import UserManager +from nilai_api.db.logs import QueryLogContext from nilai_api.handlers.nildb.handler import get_prompt_from_nildb from nilai_api.handlers.nilrag import handle_nilrag from nilai_api.handlers.tools.tool_router import handle_tool_workflow @@ -72,6 +79,7 @@ async def chat_completion( ], ) ), + background_tasks: BackgroundTasks = BackgroundTasks(), _rate_limit=Depends( RateLimit( concurrent_extractor=chat_completion_concurrent_rate_limit, @@ -80,6 +88,7 @@ async def chat_completion( ), auth_info: AuthenticationInfo = Depends(get_auth_info), meter: MeteringContext = Depends(LLMMeter), + log_ctx: QueryLogContext = Depends(QueryLogContext), ) -> Union[SignedChatCompletion, StreamingResponse]: """ Generate a chat completion response from the AI model. @@ -133,243 +142,311 @@ async def chat_completion( ) response = await chat_completion(request, user) """ - - if len(req.messages) == 0: - raise HTTPException( - status_code=400, - detail="Request contained 0 messages", - ) + # Initialize log context early so we can log any errors + log_ctx.set_user(auth_info.user.user_id) + log_ctx.set_lockid(meter.lock_id) model_name = req.model request_id = str(uuid.uuid4()) t_start = time.monotonic() - logger.info(f"[chat] call start request_id={req.messages}") - endpoint = await state.get_model(model_name) - if endpoint is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Invalid model name {model_name}, check /v1/models for options", - ) - - has_multimodal = req.has_multimodal_content() - logger.info(f"[chat] has_multimodal: {has_multimodal}") - if has_multimodal and (not endpoint.metadata.multimodal_support or req.web_search): - raise HTTPException( - status_code=400, - detail="Model does not support multimodal content, remove image inputs from request", - ) - model_url = endpoint.url + "/v1/" - - logger.info( - f"[chat] start request_id={request_id} user={auth_info.user.userid} model={model_name} stream={req.stream} web_search={bool(req.web_search)} tools={bool(req.tools)} multimodal={has_multimodal} url={model_url}" - ) - - client = AsyncOpenAI(base_url=model_url, api_key="") - if auth_info.prompt_document: - try: - nildb_prompt: str = await get_prompt_from_nildb(auth_info.prompt_document) - req.messages.insert( - 0, MessageAdapter.new_message(role="system", content=nildb_prompt) + try: + if len(req.messages) == 0: + raise HTTPException( + status_code=400, + detail="Request contained 0 messages", ) - except Exception as e: + endpoint = await state.get_model(model_name) + if endpoint is None: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=f"Unable to extract prompt from nilDB: {str(e)}", + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid model name {model_name}, check /v1/models for options", ) - if req.nilrag: - logger.info(f"[chat] nilrag start request_id={request_id}") - t_nilrag = time.monotonic() - await handle_nilrag(req) - logger.info( - f"[chat] nilrag done request_id={request_id} duration_ms={(time.monotonic() - t_nilrag) * 1000:.0f}" - ) - - messages = req.messages - sources: Optional[List[Source]] = None + # Now we have a valid model, set it in log context + log_ctx.set_model(model_name) - if req.web_search: - logger.info(f"[chat] web_search start request_id={request_id}") - t_ws = time.monotonic() - web_search_result = await handle_web_search(req, model_name, client) - messages = web_search_result.messages - sources = web_search_result.sources - logger.info( - f"[chat] web_search done request_id={request_id} sources={len(sources) if sources else 0} duration_ms={(time.monotonic() - t_ws) * 1000:.0f}" - ) - logger.info(f"[chat] web_search messages: {messages}") - - request_kwargs = { - "model": req.model, - "messages": messages, - "top_p": req.top_p, - "temperature": req.temperature, - "max_tokens": req.max_tokens, - } - - if req.tools: - if not endpoint.metadata.tool_support: + if not endpoint.metadata.tool_support and req.tools: raise HTTPException( status_code=400, detail="Model does not support tool usage, remove tools from request", ) - if model_name == "openai/gpt-oss-20b": + + has_multimodal = req.has_multimodal_content() + logger.info(f"[chat] has_multimodal: {has_multimodal}") + if has_multimodal and ( + not endpoint.metadata.multimodal_support or req.web_search + ): raise HTTPException( status_code=400, - detail="This model only supports tool calls with responses endpoint", + detail="Model does not support multimodal content, remove image inputs from request", ) - request_kwargs["tools"] = req.tools - request_kwargs["tool_choice"] = req.tool_choice - if req.stream: + model_url = endpoint.url + "/v1/" - async def chat_completion_stream_generator() -> AsyncGenerator[str, None]: - t_call = time.monotonic() - prompt_token_usage = 0 - completion_token_usage = 0 + logger.info( + f"[chat] start request_id={request_id} user={auth_info.user.user_id} model={model_name} stream={req.stream} web_search={bool(req.web_search)} tools={bool(req.tools)} multimodal={has_multimodal} url={model_url}" + ) + log_ctx.set_request_params( + temperature=req.temperature, + max_tokens=req.max_tokens, + was_streamed=req.stream or False, + was_multimodal=has_multimodal, + was_nildb=bool(auth_info.prompt_document), + was_nilrag=bool(req.nilrag), + ) + client = AsyncOpenAI(base_url=model_url, api_key="") + if auth_info.prompt_document: try: - logger.info(f"[chat] stream start request_id={request_id}") + nildb_prompt: str = await get_prompt_from_nildb( + auth_info.prompt_document + ) + req.messages.insert( + 0, MessageAdapter.new_message(role="system", content=nildb_prompt) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Unable to extract prompt from nilDB: {str(e)}", + ) + + if req.nilrag: + logger.info(f"[chat] nilrag start request_id={request_id}") + t_nilrag = time.monotonic() + await handle_nilrag(req) + logger.info( + f"[chat] nilrag done request_id={request_id} duration_ms={(time.monotonic() - t_nilrag) * 1000:.0f}" + ) - request_kwargs["stream"] = True - request_kwargs["extra_body"] = { - "stream_options": { - "include_usage": True, - "continuous_usage_stats": False, + messages = req.messages + sources: Optional[List[Source]] = None + + if req.web_search: + logger.info(f"[chat] web_search start request_id={request_id}") + t_ws = time.monotonic() + web_search_result = await handle_web_search(req, model_name, client) + messages = web_search_result.messages + sources = web_search_result.sources + logger.info( + f"[chat] web_search done request_id={request_id} sources={len(sources) if sources else 0} duration_ms={(time.monotonic() - t_ws) * 1000:.0f}" + ) + logger.info(f"[chat] web_search messages: {messages}") + + if req.stream: + + async def chat_completion_stream_generator() -> AsyncGenerator[str, None]: + t_call = time.monotonic() + prompt_token_usage = 0 + completion_token_usage = 0 + + try: + logger.info(f"[chat] stream start request_id={request_id}") + + log_ctx.start_model_timing() + + request_kwargs = { + "model": req.model, + "messages": messages, + "stream": True, + "top_p": req.top_p, + "temperature": req.temperature, + "max_tokens": req.max_tokens, + "extra_body": { + "stream_options": { + "include_usage": True, + "continuous_usage_stats": False, + } + }, } - } + if req.tools: + request_kwargs["tools"] = req.tools + + response = await client.chat.completions.create(**request_kwargs) + + async for chunk in response: + if chunk.usage is not None: + prompt_token_usage = chunk.usage.prompt_tokens + completion_token_usage = chunk.usage.completion_tokens + + payload = chunk.model_dump(exclude_unset=True) + + if chunk.usage is not None and sources: + payload["sources"] = [ + s.model_dump(mode="json") for s in sources + ] + + yield f"data: {json.dumps(payload)}\n\n" + + log_ctx.end_model_timing() + meter.set_response( + { + "usage": LLMUsage( + prompt_tokens=prompt_token_usage, + completion_tokens=completion_token_usage, + web_searches=len(sources) if sources else 0, + ) + } + ) + log_ctx.set_usage( + prompt_tokens=prompt_token_usage, + completion_tokens=completion_token_usage, + web_search_calls=len(sources) if sources else 0, + ) + background_tasks.add_task(log_ctx.commit) + logger.info( + "[chat] stream done request_id=%s prompt_tokens=%d completion_tokens=%d " + "duration_ms=%.0f total_ms=%.0f", + request_id, + prompt_token_usage, + completion_token_usage, + (time.monotonic() - t_call) * 1000, + (time.monotonic() - t_start) * 1000, + ) + + except Exception as e: + logger.error( + "[chat] stream error request_id=%s error=%s", request_id, e + ) + log_ctx.set_error(error_code=500, error_message=str(e)) + await log_ctx.commit() + yield f"data: {json.dumps({'error': 'stream_failed', 'message': str(e)})}\n\n" + + return StreamingResponse( + chat_completion_stream_generator(), + media_type="text/event-stream", + ) - response = await client.chat.completions.create(**request_kwargs) + current_messages = messages + request_kwargs = { + "model": req.model, + "messages": current_messages, # type: ignore + "top_p": req.top_p, + "temperature": req.temperature, + "max_tokens": req.max_tokens, + } + if req.tools: + request_kwargs["tools"] = req.tools # type: ignore + request_kwargs["tool_choice"] = req.tool_choice + + logger.info(f"[chat] call start request_id={request_id}") + logger.info(f"[chat] call message: {current_messages}") + t_call = time.monotonic() + log_ctx.start_model_timing() + response = await client.chat.completions.create(**request_kwargs) # type: ignore + log_ctx.end_model_timing() + logger.info( + f"[chat] call done request_id={request_id} duration_ms={(time.monotonic() - t_call) * 1000:.0f}" + ) + logger.info(f"[chat] call response: {response}") + + # Handle tool workflow fully inside tools.router + log_ctx.start_tool_timing() + ( + final_completion, + agg_prompt_tokens, + agg_completion_tokens, + ) = await handle_tool_workflow(client, req, current_messages, response) + log_ctx.end_tool_timing() + logger.info(f"[chat] call final_completion: {final_completion}") + model_response = SignedChatCompletion( + **final_completion.model_dump(), + signature="", + sources=sources, + ) - async for chunk in response: - if chunk.usage is not None: - prompt_token_usage = chunk.usage.prompt_tokens - completion_token_usage = chunk.usage.completion_tokens + logger.info( + f"[chat] model_response request_id={request_id} duration_ms={(time.monotonic() - t_call) * 1000:.0f}" + ) - payload = chunk.model_dump(exclude_unset=True) + if model_response.usage is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Model response does not contain usage statistics", + ) - if chunk.usage is not None and sources: - payload["sources"] = [ - s.model_dump(mode="json") for s in sources - ] + if agg_prompt_tokens or agg_completion_tokens: + total_prompt_tokens = response.usage.prompt_tokens + total_completion_tokens = response.usage.completion_tokens - yield f"data: {json.dumps(payload)}\n\n" + total_prompt_tokens += agg_prompt_tokens + total_completion_tokens += agg_completion_tokens - await UserManager.update_token_usage( - auth_info.user.userid, - prompt_tokens=prompt_token_usage, - completion_tokens=completion_token_usage, - ) - meter.set_response( - { - "usage": LLMUsage( - prompt_tokens=prompt_token_usage, - completion_tokens=completion_token_usage, - web_searches=len(sources) if sources else 0, - ) - } - ) - await QueryLogManager.log_query( - auth_info.user.userid, - model=req.model, - prompt_tokens=prompt_token_usage, - completion_tokens=completion_token_usage, - web_search_calls=len(sources) if sources else 0, - ) - logger.info( - "[chat] stream done request_id=%s prompt_tokens=%d completion_tokens=%d " - "duration_ms=%.0f total_ms=%.0f", - request_id, - prompt_token_usage, - completion_token_usage, - (time.monotonic() - t_call) * 1000, - (time.monotonic() - t_start) * 1000, - ) + model_response.usage.prompt_tokens = total_prompt_tokens + model_response.usage.completion_tokens = total_completion_tokens + model_response.usage.total_tokens = ( + total_prompt_tokens + total_completion_tokens + ) - except Exception as e: - logger.error( - "[chat] stream error request_id=%s error=%s", request_id, e + # Update token usage in DB + meter.set_response( + { + "usage": LLMUsage( + prompt_tokens=model_response.usage.prompt_tokens, + completion_tokens=model_response.usage.completion_tokens, + web_searches=len(sources) if sources else 0, ) - yield f"data: {json.dumps({'error': 'stream_failed', 'message': str(e)})}\n\n" - - return StreamingResponse( - chat_completion_stream_generator(), - media_type="text/event-stream", + } ) - logger.info(f"[chat] call start request_id={request_id}") - t_call = time.monotonic() - response = await client.chat.completions.create(**request_kwargs) - logger.info( - f"[chat] call done request_id={request_id} duration_ms={(time.monotonic() - t_call) * 1000:.0f}" - ) - logger.info(f"[chat] call response: {response}") - - # Handle tool workflow fully inside tools.router - ( - final_completion, - agg_prompt_tokens, - agg_completion_tokens, - ) = await handle_tool_workflow(client, req, request_kwargs["messages"], response) - logger.info(f"[chat] call final_completion: {final_completion}") - model_response = SignedChatCompletion( - **final_completion.model_dump(), - signature="", - sources=sources, - ) - - logger.info( - f"[chat] model_response request_id={request_id} duration_ms={(time.monotonic() - t_call) * 1000:.0f}" - ) + # Log query with context + tool_calls_count = 0 + if final_completion.choices and final_completion.choices[0].message.tool_calls: + tool_calls_count = len(final_completion.choices[0].message.tool_calls) - if model_response.usage is None: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Model response does not contain usage statistics", + log_ctx.set_usage( + prompt_tokens=model_response.usage.prompt_tokens, + completion_tokens=model_response.usage.completion_tokens, + tool_calls=tool_calls_count, + web_search_calls=len(sources) if sources else 0, ) + # Use background task for successful requests to avoid blocking response + background_tasks.add_task(log_ctx.commit) - if agg_prompt_tokens or agg_completion_tokens: - total_prompt_tokens = response.usage.prompt_tokens - total_completion_tokens = response.usage.completion_tokens - - total_prompt_tokens += agg_prompt_tokens - total_completion_tokens += agg_completion_tokens + # Sign the response + response_json = model_response.model_dump_json() + signature = sign_message(state.private_key, response_json) + model_response.signature = b64encode(signature).decode() - model_response.usage.prompt_tokens = total_prompt_tokens - model_response.usage.completion_tokens = total_completion_tokens - model_response.usage.total_tokens = ( - total_prompt_tokens + total_completion_tokens + logger.info( + f"[chat] done request_id={request_id} prompt_tokens={model_response.usage.prompt_tokens} completion_tokens={model_response.usage.completion_tokens} total_ms={(time.monotonic() - t_start) * 1000:.0f}" + ) + return model_response + except HTTPException as e: + # Extract error code from HTTPException, default to status code + error_code = e.status_code + error_message = str(e.detail) if e.detail else str(e) + logger.error( + f"[chat] HTTPException request_id={request_id} user={auth_info.user.user_id} " + f"model={model_name} error_code={error_code} error={error_message}", + exc_info=True, ) - # Update token usage in DB - await UserManager.update_token_usage( - auth_info.user.userid, - prompt_tokens=model_response.usage.prompt_tokens, - completion_tokens=model_response.usage.completion_tokens, - ) - meter.set_response( - { - "usage": LLMUsage( - prompt_tokens=model_response.usage.prompt_tokens, - completion_tokens=model_response.usage.completion_tokens, - web_searches=len(sources) if sources else 0, - ) - } - ) - await QueryLogManager.log_query( - auth_info.user.userid, - model=req.model, - prompt_tokens=model_response.usage.prompt_tokens, - completion_tokens=model_response.usage.completion_tokens, - web_search_calls=len(sources) if sources else 0, - ) - - # Sign the response - response_json = model_response.model_dump_json() - signature = sign_message(state.private_key, response_json) - model_response.signature = b64encode(signature).decode() - - logger.info( - f"[chat] done request_id={request_id} prompt_tokens={model_response.usage.prompt_tokens} completion_tokens={model_response.usage.completion_tokens} total_ms={(time.monotonic() - t_start) * 1000:.0f}" - ) - return model_response + # Only log server errors (5xx) to database to prevent DoS attacks via client errors + # Client errors (4xx) are logged to application logs only + if error_code >= 500: + # Set model if not already set (e.g., for validation errors before model validation) + if log_ctx.model is None: + log_ctx.set_model(model_name) + log_ctx.set_error(error_code=error_code, error_message=error_message) + await log_ctx.commit() + # For 4xx errors, we skip DB logging - they're logged above via logger.error() + # This prevents DoS attacks where attackers send many invalid requests + + raise + except Exception as e: + # Catch any other unexpected exceptions + error_message = str(e) + logger.error( + f"[chat] unexpected error request_id={request_id} user={auth_info.user.user_id} " + f"model={model_name} error={error_message}", + exc_info=True, + ) + # Set model if not already set + if log_ctx.model is None: + log_ctx.set_model(model_name) + log_ctx.set_error(error_code=500, error_message=error_message) + await log_ctx.commit() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Internal server error: {error_message}", + ) diff --git a/nilai-api/src/nilai_api/routers/endpoints/responses.py b/nilai-api/src/nilai_api/routers/endpoints/responses.py index 1b764674..54f9ca92 100644 --- a/nilai-api/src/nilai_api/routers/endpoints/responses.py +++ b/nilai-api/src/nilai_api/routers/endpoints/responses.py @@ -5,7 +5,15 @@ from base64 import b64encode from typing import AsyncGenerator, Optional, Union, List, Tuple -from fastapi import APIRouter, Body, Depends, HTTPException, status, Request +from fastapi import ( + APIRouter, + Body, + Depends, + HTTPException, + status, + Request, + BackgroundTasks, +) from fastapi.responses import StreamingResponse from openai import AsyncOpenAI @@ -13,8 +21,7 @@ from nilai_api.config import CONFIG from nilai_api.crypto import sign_message from nilai_api.credit import LLMMeter, LLMUsage -from nilai_api.db.logs import QueryLogManager -from nilai_api.db.users import UserManager +from nilai_api.db.logs import QueryLogContext from nilai_api.handlers.nildb.handler import get_prompt_from_nildb # from nilai_api.handlers.nilrag import handle_nilrag_for_responses @@ -73,6 +80,7 @@ async def create_response( "web_search": False, } ), + background_tasks: BackgroundTasks = BackgroundTasks(), _rate_limit=Depends( RateLimit( concurrent_extractor=responses_concurrent_rate_limit, @@ -81,6 +89,7 @@ async def create_response( ), auth_info: AuthenticationInfo = Depends(get_auth_info), meter: MeteringContext = Depends(LLMMeter), + log_ctx: QueryLogContext = Depends(QueryLogContext), ) -> Union[SignedResponse, StreamingResponse]: """ Generate a response from the AI model using the Responses API. @@ -124,225 +133,289 @@ async def create_response( - **500 Internal Server Error**: - Model fails to generate a response """ - if not req.input: - raise HTTPException( - status_code=400, - detail="Request 'input' field cannot be empty.", - ) + # Initialize log context early so we can log any errors + log_ctx.set_user(auth_info.user.user_id) + log_ctx.set_lockid(meter.lock_id) model_name = req.model request_id = str(uuid.uuid4()) t_start = time.monotonic() - endpoint = await state.get_model(model_name) - if endpoint is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Invalid model name {model_name}, check /v1/models for options", - ) + try: + if not req.input: + raise HTTPException( + status_code=400, + detail="Request 'input' field cannot be empty.", + ) - if not endpoint.metadata.tool_support and req.tools: - raise HTTPException( - status_code=400, - detail="Model does not support tool usage, remove tools from request", - ) + endpoint = await state.get_model(model_name) + if endpoint is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid model name {model_name}, check /v1/models for options", + ) - has_multimodal = req.has_multimodal_content() - if has_multimodal and (not endpoint.metadata.multimodal_support or req.web_search): - raise HTTPException( - status_code=400, - detail="Model does not support multimodal content, remove image inputs from request", - ) + # Now we have a valid model, set it in log context + log_ctx.set_model(model_name) - model_url = endpoint.url + "/v1/" + if not endpoint.metadata.tool_support and req.tools: + raise HTTPException( + status_code=400, + detail="Model does not support tool usage, remove tools from request", + ) - client = AsyncOpenAI(base_url=model_url, api_key="") - if auth_info.prompt_document: - try: - nildb_prompt: str = await get_prompt_from_nildb(auth_info.prompt_document) - req.ensure_instructions(nildb_prompt) - except Exception as e: + has_multimodal = req.has_multimodal_content() + if has_multimodal and ( + not endpoint.metadata.multimodal_support or req.web_search + ): raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=f"Unable to extract prompt from nilDB: {str(e)}", + status_code=400, + detail="Model does not support multimodal content, remove image inputs from request", ) - input_items = req.input - instructions = req.instructions - sources: Optional[List[Source]] = None + model_url = endpoint.url + "/v1/" - if req.web_search: - logger.info(f"[responses] web_search start request_id={request_id}") - t_ws = time.monotonic() - web_search_result = await handle_web_search_for_responses( - req, model_name, client - ) - input_items = web_search_result.input - instructions = web_search_result.instructions - sources = web_search_result.sources logger.info( - f"[responses] web_search done request_id={request_id} sources={len(sources) if sources else 0} duration_ms={(time.monotonic() - t_ws) * 1000:.0f}" + f"[responses] start request_id={request_id} user={auth_info.user.user_id} model={model_name} stream={req.stream} web_search={bool(req.web_search)} tools={bool(req.tools)} multimodal={has_multimodal} url={model_url}" + ) + log_ctx.set_request_params( + temperature=req.temperature, + max_tokens=req.max_output_tokens, + was_streamed=req.stream or False, + was_multimodal=has_multimodal, + was_nildb=bool(auth_info.prompt_document), + was_nilrag=False, ) - if req.stream: - - async def response_stream_generator() -> AsyncGenerator[str, None]: - t_call = time.monotonic() - prompt_token_usage = 0 - completion_token_usage = 0 - + client = AsyncOpenAI(base_url=model_url, api_key="") + if auth_info.prompt_document: try: - logger.info(f"[responses] stream start request_id={request_id}") - request_kwargs = { - "model": req.model, - "input": input_items, - "instructions": instructions, - "stream": True, - "top_p": req.top_p, - "temperature": req.temperature, - "max_output_tokens": req.max_output_tokens, - "extra_body": { - "stream_options": { - "include_usage": True, - "continuous_usage_stats": False, - } - }, - } - if req.tools: - request_kwargs["tools"] = req.tools - - stream = await client.responses.create(**request_kwargs) - - async for event in stream: - payload = event.model_dump(exclude_unset=True) - - if isinstance(event, ResponseCompletedEvent): - if event.response and event.response.usage: - usage = event.response.usage - prompt_token_usage = usage.input_tokens - completion_token_usage = usage.output_tokens - payload["response"]["usage"] = usage.model_dump(mode="json") - - if sources: - if "data" not in payload: - payload["data"] = {} - payload["data"]["sources"] = [ - s.model_dump(mode="json") for s in sources - ] - - yield f"data: {json.dumps(payload)}\n\n" - - await UserManager.update_token_usage( - auth_info.user.userid, - prompt_tokens=prompt_token_usage, - completion_tokens=completion_token_usage, - ) - meter.set_response( - { - "usage": LLMUsage( - prompt_tokens=prompt_token_usage, - completion_tokens=completion_token_usage, - web_searches=len(sources) if sources else 0, - ) - } - ) - await QueryLogManager.log_query( - auth_info.user.userid, - model=req.model, - prompt_tokens=prompt_token_usage, - completion_tokens=completion_token_usage, - web_search_calls=len(sources) if sources else 0, + nildb_prompt: str = await get_prompt_from_nildb( + auth_info.prompt_document ) - logger.info( - "[responses] stream done request_id=%s prompt_tokens=%d completion_tokens=%d duration_ms=%.0f total_ms=%.0f", - request_id, - prompt_token_usage, - completion_token_usage, - (time.monotonic() - t_call) * 1000, - (time.monotonic() - t_start) * 1000, - ) - + req.ensure_instructions(nildb_prompt) except Exception as e: - logger.error( - "[responses] stream error request_id=%s error=%s", request_id, e + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Unable to extract prompt from nilDB: {str(e)}", ) - yield f"data: {json.dumps({'error': 'stream_failed', 'message': str(e)})}\n\n" - return StreamingResponse( - response_stream_generator(), media_type="text/event-stream" - ) + input_items = req.input + instructions = req.instructions + sources: Optional[List[Source]] = None - request_kwargs = { - "model": req.model, - "input": input_items, - "instructions": instructions, - "top_p": req.top_p, - "temperature": req.temperature, - "max_output_tokens": req.max_output_tokens, - } - if req.tools: - request_kwargs["tools"] = req.tools - request_kwargs["tool_choice"] = req.tool_choice - - logger.info(f"[responses] call start request_id={request_id}") - t_call = time.monotonic() - - response = await client.responses.create(**request_kwargs) - logger.info( - f"[responses] call done request_id={request_id} duration_ms={(time.monotonic() - t_call) * 1000:.0f}" - ) + if req.web_search: + logger.info(f"[responses] web_search start request_id={request_id}") + t_ws = time.monotonic() + web_search_result = await handle_web_search_for_responses( + req, model_name, client + ) + input_items = web_search_result.input + instructions = web_search_result.instructions + sources = web_search_result.sources + logger.info( + f"[responses] web_search done request_id={request_id} sources={len(sources) if sources else 0} duration_ms={(time.monotonic() - t_ws) * 1000:.0f}" + ) - ( - final_response, - agg_prompt_tokens, - agg_completion_tokens, - ) = await handle_responses_tool_workflow(client, req, input_items, response) + if req.stream: + + async def response_stream_generator() -> AsyncGenerator[str, None]: + t_call = time.monotonic() + prompt_token_usage = 0 + completion_token_usage = 0 + + try: + logger.info(f"[responses] stream start request_id={request_id}") + log_ctx.start_model_timing() + + request_kwargs = { + "model": req.model, + "input": input_items, + "instructions": instructions, + "stream": True, + "top_p": req.top_p, + "temperature": req.temperature, + "max_output_tokens": req.max_output_tokens, + "extra_body": { + "stream_options": { + "include_usage": True, + "continuous_usage_stats": False, + } + }, + } + if req.tools: + request_kwargs["tools"] = req.tools + + stream = await client.responses.create(**request_kwargs) + + async for event in stream: + payload = event.model_dump(exclude_unset=True) + + if isinstance(event, ResponseCompletedEvent): + if event.response and event.response.usage: + usage = event.response.usage + prompt_token_usage = usage.input_tokens + completion_token_usage = usage.output_tokens + payload["response"]["usage"] = usage.model_dump( + mode="json" + ) + + if sources: + if "data" not in payload: + payload["data"] = {} + payload["data"]["sources"] = [ + s.model_dump(mode="json") for s in sources + ] + + yield f"data: {json.dumps(payload)}\n\n" + + log_ctx.end_model_timing() + meter.set_response( + { + "usage": LLMUsage( + prompt_tokens=prompt_token_usage, + completion_tokens=completion_token_usage, + web_searches=len(sources) if sources else 0, + ) + } + ) + log_ctx.set_usage( + prompt_tokens=prompt_token_usage, + completion_tokens=completion_token_usage, + web_search_calls=len(sources) if sources else 0, + ) + background_tasks.add_task(log_ctx.commit) + logger.info( + "[responses] stream done request_id=%s prompt_tokens=%d completion_tokens=%d duration_ms=%.0f total_ms=%.0f", + request_id, + prompt_token_usage, + completion_token_usage, + (time.monotonic() - t_call) * 1000, + (time.monotonic() - t_start) * 1000, + ) + + except Exception as e: + logger.error( + "[responses] stream error request_id=%s error=%s", request_id, e + ) + log_ctx.set_error(error_code=500, error_message=str(e)) + await log_ctx.commit() + yield f"data: {json.dumps({'error': 'stream_failed', 'message': str(e)})}\n\n" + + return StreamingResponse( + response_stream_generator(), media_type="text/event-stream" + ) - model_response = SignedResponse( - **final_response.model_dump(), - signature="", - sources=sources, - ) + request_kwargs = { + "model": req.model, + "input": input_items, + "instructions": instructions, + "top_p": req.top_p, + "temperature": req.temperature, + "max_output_tokens": req.max_output_tokens, + } + if req.tools: + request_kwargs["tools"] = req.tools + request_kwargs["tool_choice"] = req.tool_choice + + logger.info(f"[responses] call start request_id={request_id}") + t_call = time.monotonic() + log_ctx.start_model_timing() + response = await client.responses.create(**request_kwargs) + log_ctx.end_model_timing() + logger.info( + f"[responses] call done request_id={request_id} duration_ms={(time.monotonic() - t_call) * 1000:.0f}" + ) - if model_response.usage is None: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Model response does not contain usage statistics", + # Handle tool workflow + log_ctx.start_tool_timing() + ( + final_response, + agg_prompt_tokens, + agg_completion_tokens, + ) = await handle_responses_tool_workflow(client, req, input_items, response) + log_ctx.end_tool_timing() + + model_response = SignedResponse( + **final_response.model_dump(), + signature="", + sources=sources, ) - if agg_prompt_tokens or agg_completion_tokens: - model_response.usage.input_tokens += agg_prompt_tokens - model_response.usage.output_tokens += agg_completion_tokens + if model_response.usage is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Model response does not contain usage statistics", + ) - prompt_tokens = model_response.usage.input_tokens - completion_tokens = model_response.usage.output_tokens + if agg_prompt_tokens or agg_completion_tokens: + model_response.usage.input_tokens += agg_prompt_tokens + model_response.usage.output_tokens += agg_completion_tokens - await UserManager.update_token_usage( - auth_info.user.userid, - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - ) - meter.set_response( - { - "usage": LLMUsage( - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - web_searches=len(sources) if sources else 0, - ) - } - ) - await QueryLogManager.log_query( - auth_info.user.userid, - model=req.model, - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - web_search_calls=len(sources) if sources else 0, - ) + prompt_tokens = model_response.usage.input_tokens + completion_tokens = model_response.usage.output_tokens - response_json = model_response.model_dump_json() - signature = sign_message(state.private_key, response_json) - model_response.signature = b64encode(signature).decode() + meter.set_response( + { + "usage": LLMUsage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + web_searches=len(sources) if sources else 0, + ) + } + ) - logger.info( - f"[responses] done request_id={request_id} prompt_tokens={prompt_tokens} completion_tokens={completion_tokens} total_ms={(time.monotonic() - t_start) * 1000:.0f}" - ) - return model_response + # Log query with context + # Note: Response object structure for tools might differ from Chat, + # but we'll assume basic usage logging is sufficient or adapt if needed. + # For now, we don't count tool calls explicitly in log_ctx for responses unless we parse them. + # Chat.py does: tool_calls_count = len(final_completion.choices[0].message.tool_calls) + # Responses API structure is different. `final_response` is a Response object. + # It might not have 'choices'. It has 'output'. + + log_ctx.set_usage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + web_search_calls=len(sources) if sources else 0, + ) + background_tasks.add_task(log_ctx.commit) + + response_json = model_response.model_dump_json() + signature = sign_message(state.private_key, response_json) + model_response.signature = b64encode(signature).decode() + + logger.info( + f"[responses] done request_id={request_id} prompt_tokens={prompt_tokens} completion_tokens={completion_tokens} total_ms={(time.monotonic() - t_start) * 1000:.0f}" + ) + return model_response + + except HTTPException as e: + error_code = e.status_code + error_message = str(e.detail) if e.detail else str(e) + logger.error( + f"[responses] HTTPException request_id={request_id} user={auth_info.user.user_id} " + f"model={model_name} error_code={error_code} error={error_message}", + exc_info=True, + ) + + if error_code >= 500: + if log_ctx.model is None: + log_ctx.set_model(model_name) + log_ctx.set_error(error_code=error_code, error_message=error_message) + await log_ctx.commit() + + raise + except Exception as e: + error_message = str(e) + logger.error( + f"[responses] unexpected error request_id={request_id} user={auth_info.user.user_id} " + f"model={model_name} error={error_message}", + exc_info=True, + ) + if log_ctx.model is None: + log_ctx.set_model(model_name) + log_ctx.set_error(error_code=500, error_message=error_message) + await log_ctx.commit() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Internal server error: {error_message}", + ) diff --git a/nilai-api/src/nilai_api/routers/private.py b/nilai-api/src/nilai_api/routers/private.py index b2956553..5cbcbf64 100644 --- a/nilai-api/src/nilai_api/routers/private.py +++ b/nilai-api/src/nilai_api/routers/private.py @@ -5,6 +5,7 @@ from nilai_api.attestation import get_attestation_report from nilai_api.auth import get_auth_info, AuthenticationInfo +from nilai_api.db.logs import QueryLogManager from nilai_api.handlers.nildb.api_model import ( PromptDelegationRequest, PromptDelegationToken, @@ -17,7 +18,6 @@ from nilai_common import ( AttestationReport, ModelMetadata, - Nonce, Usage, ) @@ -32,14 +32,10 @@ @router.get("/v1/delegation") async def get_prompt_store_delegation( prompt_delegation_request: PromptDelegationRequest, - auth_info: AuthenticationInfo = Depends(get_auth_info), + _: AuthenticationInfo = Depends( + get_auth_info + ), # This is to satisfy that the user is authenticated ) -> PromptDelegationToken: - if not auth_info.user.is_subscription_owner: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Prompt storage is reserved to subscription owners", - ) - try: return await get_nildb_delegation_token(prompt_delegation_request) except Exception as e: @@ -63,23 +59,26 @@ async def get_usage(auth_info: AuthenticationInfo = Depends(get_auth_info)) -> U usage = await get_usage(user) ``` """ - return Usage( - prompt_tokens=auth_info.user.prompt_tokens, - completion_tokens=auth_info.user.completion_tokens, - total_tokens=auth_info.user.prompt_tokens + auth_info.user.completion_tokens, - queries=auth_info.user.queries, # type: ignore # FIXME this field is not part of Usage + user_usage: Optional[Usage] = await QueryLogManager.get_user_token_usage( + auth_info.user.user_id ) + if user_usage is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found", + ) + return user_usage @router.get("/v1/attestation/report", tags=["Attestation"]) async def get_attestation( - nonce: Optional[Nonce] = None, + nonce: str, auth_info: AuthenticationInfo = Depends(get_auth_info), ) -> AttestationReport: """ Generate a cryptographic attestation report. - - **attestation_request**: Attestation request containing a nonce + - **nonce**: Nonce for the attestation request (64 character hex string) - **user**: Authenticated user information (through HTTP Bearer header) - **Returns**: Attestation details for service verification diff --git a/nilai-api/src/nilai_api/routers/public.py b/nilai-api/src/nilai_api/routers/public.py index 2a6b09bc..c198da5c 100644 --- a/nilai-api/src/nilai_api/routers/public.py +++ b/nilai-api/src/nilai_api/routers/public.py @@ -3,8 +3,7 @@ from nilai_api.state import state # Internal libraries -from nilai_common import HealthCheckResponse, AttestationReport -from nilai_api.attestation import verify_attestation_report +from nilai_common import HealthCheckResponse router = APIRouter() @@ -42,14 +41,3 @@ async def health_check() -> HealthCheckResponse: ``` """ return HealthCheckResponse(status="ok", uptime=state.uptime) - - -@router.post("/attestation/verify", tags=["Attestation"]) -async def post_attestation(attestation_report: AttestationReport) -> bool: - """ - Verify a cryptographic attestation report. - - - **attestation_report**: Attestation report to verify - - **Returns**: True if the attestation report is valid, False otherwise - """ - return await verify_attestation_report(attestation_report) diff --git a/nilai-api/src/nilai_api/state.py b/nilai-api/src/nilai_api/state.py index cf046c74..37385a65 100644 --- a/nilai-api/src/nilai_api/state.py +++ b/nilai-api/src/nilai_api/state.py @@ -17,10 +17,17 @@ def __init__(self): self.sem = Semaphore(2) self.discovery_service = ModelServiceDiscovery( - host=CONFIG.etcd.host, port=CONFIG.etcd.port + host=CONFIG.discovery.host, port=CONFIG.discovery.port ) + self._discovery_initialized = False self._uptime = time.time() + async def _ensure_discovery_initialized(self): + """Ensure discovery service is initialized.""" + if not self._discovery_initialized: + await self.discovery_service.initialize() + self._discovery_initialized = True + @property def uptime(self): elapsed_time = time.time() - self._uptime @@ -42,11 +49,13 @@ def uptime(self): @property async def models(self) -> Dict[str, ModelEndpoint]: + await self._ensure_discovery_initialized() return await self.discovery_service.discover_models() async def get_model(self, model_id: str) -> Optional[ModelEndpoint]: if model_id is None or len(model_id) == 0: return None + await self._ensure_discovery_initialized() return await self.discovery_service.get_model(model_id) diff --git a/nilai-attestation/README.md b/nilai-attestation/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/nilai-attestation/gunicorn.conf.py b/nilai-attestation/gunicorn.conf.py deleted file mode 100644 index fd58ad78..00000000 --- a/nilai-attestation/gunicorn.conf.py +++ /dev/null @@ -1,18 +0,0 @@ -# gunicorn.config.py -from nilai_common.config import SETTINGS - -# Bind to address and port -bind = [f"0.0.0.0:{SETTINGS.attestation_port}"] - -# Set the number of workers (2) -workers = 1 - - -# Set the number of threads per worker (16) -threads = 1 - -# Set the timeout (120 seconds) -timeout = 120 - -# Set the worker class to UvicornWorker for async handling -worker_class = "uvicorn.workers.UvicornWorker" diff --git a/nilai-attestation/pyproject.toml b/nilai-attestation/pyproject.toml deleted file mode 100644 index c11ef91b..00000000 --- a/nilai-attestation/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[project] -name = "nilai-attestation" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -authors = [ - { name = "JosĆ© Cabrero-Holgueras", email = "jose.cabrero@nillion.com" } -] -requires-python = "==3.12.*" -dependencies = [ - "fastapi>=0.115.12", - "gunicorn>=23.0.0", - "nilai-common", - "nv-attestation-sdk==2.4.0", - "uvicorn>=0.34.0", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv.sources] -nilai-common = { path = "../packages/nilai-common", editable = true } diff --git a/nilai-attestation/src/nilai_attestation/__init__.py b/nilai-attestation/src/nilai_attestation/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/nilai-attestation/src/nilai_attestation/app.py b/nilai-attestation/src/nilai_attestation/app.py deleted file mode 100644 index 7fb8e8f1..00000000 --- a/nilai-attestation/src/nilai_attestation/app.py +++ /dev/null @@ -1,51 +0,0 @@ -# Fast API and serving - -from fastapi import FastAPI -from nilai_attestation.routers import private, public - -# Fast API and serving - - -import logging - -logging.getLogger("nv_attestation_sdk").setLevel(logging.WARNING) -logging.getLogger("sdk-logger").setLevel(logging.WARNING) -logging.getLogger("sdk-console").setLevel(logging.WARNING) -logging.getLogger("sdk-file").setLevel(logging.WARNING) -logging.getLogger("gpu-verifier-event").setLevel(logging.WARNING) -logging.getLogger("gpu-verifier-info").setLevel(logging.WARNING) - - -description = """ -An AI model serving platform powered by secure, confidential computing. - -## Easy API Client Generation - -Want to use our API in your project? Great news! You can automatically generate a client library in just a few simple steps using the OpenAPI specification. -``` -After generating, you'll have a fully functional client library that makes it easy to interact with our AI services. No more manual API request handling! -""" -app = FastAPI( - title="NilAI attestation", - description=description, - version="0.1.0", - terms_of_service="https://nillion.com", - contact={ - "name": "Nillion AI Support", - "email": "jose.cabrero@nillion.com", - }, - license_info={ - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0", - }, - openapi_tags=[ - { - "name": "Attestation", - "description": "Retrieve cryptographic attestation information for service verification", - } - ], -) - - -app.include_router(private.router) -app.include_router(public.router) diff --git a/nilai-attestation/src/nilai_attestation/attestation/__init__.py b/nilai-attestation/src/nilai_attestation/attestation/__init__.py deleted file mode 100644 index d7ee6a7b..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -from functools import lru_cache -from nilai_common import AttestationReport, Nonce - -from nilai_attestation.attestation.sev.sev import sev -from nilai_attestation.attestation.nvtrust.nv_attester import nv_attest -from nilai_attestation.attestation.nvtrust.nv_verifier import verify_attestation -from nilai_common.logger import setup_logger - -logger = setup_logger(__name__) - - -@lru_cache(maxsize=1) -def load_sev_library() -> bool: - """Load the SEV library""" - return sev.init() - - -def get_attestation_report(nonce: Nonce | None = None) -> AttestationReport: - """Get the attestation report for the given nonce""" - - # Since Nonce is an Annotated[str], we can use it directly - attestation_nonce: Nonce = "0" * 64 if nonce is None else nonce - - logger.info(f"Nonce: {attestation_nonce}") - - load_sev_library() - - return AttestationReport( - nonce=attestation_nonce, - verifying_key="", - cpu_attestation=sev.get_quote(), - gpu_attestation=nv_attest(attestation_nonce), - ) - - -def verify_attestation_report(report: AttestationReport) -> bool: - """Verify the attestation report""" - return verify_attestation(report) - - -if __name__ == "__main__": - nonce = "0" * 64 - report = get_attestation_report(nonce) - print(report) diff --git a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/__init__.py b/nilai-attestation/src/nilai_attestation/attestation/nvtrust/__init__.py deleted file mode 100644 index 1cff37ad..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/__init__.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Attester: Generate an attestation token from local evidence -# Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -from nv_attestation_sdk import attestation # type: ignore - -import subprocess -from functools import lru_cache -import logging - -logger = logging.getLogger(__name__) - -NRAS_URL = "https://nras.attestation.nvidia.com/v3/attest/gpu" -OCSP_URL = "https://ocsp.ndis.nvidia.com/" -RIM_URL = "https://rim.attestation.nvidia.com/v1/rim/" - - -@lru_cache(maxsize=1) -def is_nvidia_gpu_available() -> bool: - """Check if an NVIDIA GPU with compute capability is available in the system and cache the result. - - Returns: - bool: True if an NVIDIA GPU is available and compute capability is ON, False otherwise. - """ - try: - # Run the command and capture its output - result = subprocess.run( - ["nvidia-smi", "conf-compute", "-f"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=True, - text=True, # ensures stdout/stderr are strings not bytes - ) - - output = result.stdout.strip() - if "ON" in output: - return True - else: - return False - - except (subprocess.CalledProcessError, FileNotFoundError): - return False - - -@lru_cache(maxsize=1) -def get_client() -> attestation.Attestation: - """Create and configure the attestation client with appropriate verifiers. - - This function initializes an attestation client and configures it based on the availability - of an NVIDIA GPU. If a GPU is available, a remote verifier is added. Otherwise, a local - verifier is configured. - - Returns: - attestation.Attestation: A configured attestation client instance. - """ - # Create and configure the attestation client. - client = attestation.Attestation() - client.set_name("nilai-attestation-module") - logger.info("Checking if NVIDIA GPU is available") - - if is_nvidia_gpu_available(): - logger.info("NVIDIA GPU is available") - # Configure the remote verifier. - # WARNING: The next statement happens at a global level. It shall only be done once. - client.add_verifier( - attestation.Devices.GPU, attestation.Environment.REMOTE, NRAS_URL, "" - ) - else: - logger.info("NVIDIA GPU is not available") - # WARNING: The next statement happens at a global level. It shall only be done once. - client.add_verifier( - attestation.Devices.GPU, - attestation.Environment.LOCAL, - "", - "", - OCSP_URL, - RIM_URL, - ) - return client diff --git a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_attester.py b/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_attester.py deleted file mode 100644 index 9d1c4daf..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_attester.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Attester: Generate an attestation token from local evidence -# Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -from nilai_attestation.attestation.nvtrust import is_nvidia_gpu_available, get_client -import base64 -from nilai_common import Nonce, NVAttestationToken -import logging - -logger = logging.getLogger(__name__) - - -def nv_attest(nonce: Nonce) -> NVAttestationToken: - """Generate an attestation token from local evidence. - - Args: - nonce: The nonce to be used for the attestation - - Returns: - NVAttestationToken: The attestation token response - """ - client = get_client() - client.set_nonce(nonce) - - evidence_list = [] - - # Collect evidence and perform attestation. - options = {} - if not is_nvidia_gpu_available(): - options["no_gpu_mode"] = True - - evidence_list = client.get_evidence(options=options) - logger.info(f"Evidence list: {evidence_list}") - - # Attestation result - attestation_result = client.attest(evidence_list) - - logger.info(f"Attestation result: {attestation_result}") - - # Retrieve the attestation token and return it wrapped in our model - token: str = client.get_token() - - b64_token: NVAttestationToken = base64.b64encode(token.encode("utf-8")).decode( - "utf-8" - ) - logger.info(f"Token: {b64_token}") - return b64_token diff --git a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_verifier.py b/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_verifier.py deleted file mode 100644 index 9131507f..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/nvtrust/nv_verifier.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Verifier: Validate an attestation token against a remote policy -# Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -from nilai_common.api_models import AttestationReport -import json -import base64 -from nilai_common.logger import setup_logger -from nilai_attestation.attestation.nvtrust import get_client - -logger = setup_logger(__name__) - -NRAS_URL = "https://nras.attestation.nvidia.com/v3/attest/gpu" - - -POLICY = { - "version": "3.0", - "authorization-rules": { - "type": "JWT", - "overall-claims": {"x-nvidia-overall-att-result": True, "x-nvidia-ver": "2.0"}, - "detached-claims": { - "measres": "success", - "x-nvidia-gpu-arch-check": True, - "x-nvidia-gpu-attestation-report-cert-chain-validated": True, - "x-nvidia-gpu-attestation-report-parsed": True, - "x-nvidia-gpu-attestation-report-nonce-match": True, - "x-nvidia-gpu-attestation-report-signature-verified": True, - "x-nvidia-gpu-driver-rim-fetched": True, - "x-nvidia-gpu-driver-rim-schema-validated": True, - "x-nvidia-gpu-driver-rim-cert-validated": True, - "x-nvidia-gpu-driver-rim-signature-verified": True, - "x-nvidia-gpu-driver-rim-measurements-available": True, - "x-nvidia-gpu-vbios-rim-fetched": True, - "x-nvidia-gpu-vbios-rim-schema-validated": True, - "x-nvidia-gpu-vbios-rim-cert-validated": True, - "x-nvidia-gpu-vbios-rim-signature-verified": True, - "x-nvidia-gpu-vbios-rim-measurements-available": True, - "x-nvidia-gpu-vbios-index-no-conflict": True, - }, - }, -} - - -def verify_attestation(attestation_report: AttestationReport) -> bool: - """Verify an NVIDIA attestation token against a policy. - - Args: - token: The attestation token to verify - policy_path: Optional path to the policy file. If not provided, uses default policy. - - Returns: - bool: True if the token is valid according to the policy, False otherwise. - """ - - # Create an attestation client instance for token verification. - logger.info(f"Attestation report: {attestation_report}") - client = get_client() - client.set_nonce(attestation_report.nonce) - - token = base64.b64decode(attestation_report.gpu_attestation).decode("utf-8") - logger.info(f"Token: {token}") - try: - validation_result = client.validate_token(json.dumps(POLICY), token) - logger.info(f"Token validation result: {validation_result}") - - return validation_result - - except Exception as e: - logger.error(f"Failed to verify attestation token: {e}") - return False diff --git a/nilai-attestation/src/nilai_attestation/attestation/sev/.gitignore b/nilai-attestation/src/nilai_attestation/attestation/sev/.gitignore deleted file mode 100644 index 0ee971a8..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/sev/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -libsevguest.h -libsevguest.so -sev diff --git a/nilai-attestation/src/nilai_attestation/attestation/sev/README.md b/nilai-attestation/src/nilai_attestation/attestation/sev/README.md deleted file mode 100644 index 687610ff..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/sev/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# How to build? - -``` -go build -o libsevguest.so -buildmode=c-shared main.go -``` diff --git a/nilai-attestation/src/nilai_attestation/attestation/sev/__init__.py b/nilai-attestation/src/nilai_attestation/attestation/sev/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/nilai-attestation/src/nilai_attestation/attestation/sev/go.mod b/nilai-attestation/src/nilai_attestation/attestation/sev/go.mod deleted file mode 100644 index ba30acf9..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/sev/go.mod +++ /dev/null @@ -1,15 +0,0 @@ -module nillion/sev - -go 1.23.3 - -require github.com/google/go-sev-guest v0.11.2-0.20241122204452-64cd695124b1 - -require ( - github.com/google/go-configfs-tsm v0.2.2 // indirect - github.com/google/logger v1.1.1 // indirect - github.com/google/uuid v1.6.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sys v0.15.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect -) diff --git a/nilai-attestation/src/nilai_attestation/attestation/sev/go.sum b/nilai-attestation/src/nilai_attestation/attestation/sev/go.sum deleted file mode 100644 index 2bbee2d6..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/sev/go.sum +++ /dev/null @@ -1,29 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-configfs-tsm v0.2.2 h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt215EWcs98= -github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= -github.com/google/go-sev-guest v0.11.2-0.20241122204452-64cd695124b1 h1:K33T2ardZgY4LVxPakM85KSip9aag2jTwmOZs4i1dJg= -github.com/google/go-sev-guest v0.11.2-0.20241122204452-64cd695124b1/go.mod h1:8+UOtSaqVIZjJJ9DDmgRko3J/kNc6jI5KLHxoeao7cA= -github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= -github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/nilai-attestation/src/nilai_attestation/attestation/sev/main.go b/nilai-attestation/src/nilai_attestation/attestation/sev/main.go deleted file mode 100644 index 6218d16c..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/sev/main.go +++ /dev/null @@ -1,108 +0,0 @@ -package main - -// #include -// #include -import "C" -import ( - "fmt" - "unsafe" - - "github.com/google/go-sev-guest/client" - "github.com/google/go-sev-guest/verify" -) - -var device client.Device -var quoteProvider client.QuoteProvider - -//export OpenDevice -func OpenDevice() int { - var err error - if device, err = client.OpenDevice(); err != nil { - fmt.Printf("failed to open device: %v\n", err) - return -1 - } - return 0 -} - -//export GetQuoteProvider -func GetQuoteProvider() int { - var err error - if quoteProvider, err = client.GetQuoteProvider(); err != nil { - fmt.Printf("failed to get quote provider: %v\n", err) - return -1 - } - return 0 -} - -//export Init -func Init() int { - if OpenDevice() != 0 || GetQuoteProvider() != 0 { - return -1 - } - return 0 -} - -//export GetQuote -func GetQuote(reportData *C.char) *C.char { - if reportData == nil { - return nil - } - - // Convert reportData to a Go byte slice. - var reportDataBytes [64]byte - for i := 0; i < 64; i++ { - reportDataBytes[i] = byte(C.char(*(*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(reportData)) + uintptr(i))))) - } - - // Get the quote using the provided QuoteProvider. - quote, err := client.GetQuoteProto(quoteProvider, reportDataBytes) - if err != nil { - return nil - } - result := C.CString(quote.GetReport().String()) - //result := C.CString(quote.String()) - return result -} - -//export VerifyQuote -func VerifyQuote(quoteStr *C.char) int { - // Change the quoteStr from C.char to string - report := []byte(C.GoString(quoteStr)) - - err := verify.RawSnpReport(report, verify.DefaultOptions()) - if err != nil { - fmt.Printf("failed to verify quote: %v\n", err) - return -1 - } - return 0 -} - -func test() { - Init() - - // Transform the reportData from C.char to []byte - reportDataBytes := [64]byte{0} - - quote2, err := client.GetQuoteProto(quoteProvider, reportDataBytes) - if err != nil { - panic("B") - } - - err = verify.SnpReport(quote2.GetReport(), verify.DefaultOptions()) - if err != nil { - fmt.Printf("failed to verify report: %v\n", err) - } - // Use the device to get a quote. - //quote, err := quoteProvider.GetRawQuote(*reportDataBytes) - quote, err := client.GetQuoteProto(quoteProvider, reportDataBytes) - if err != nil { - panic("A") - } - quote_bytes := []byte(quote.String()) - err = verify.RawSnpReport(quote_bytes, verify.DefaultOptions()) - - if err != nil { - panic("Failed to verify quote") - } -} -func main() {} diff --git a/nilai-attestation/src/nilai_attestation/attestation/sev/sev.py b/nilai-attestation/src/nilai_attestation/attestation/sev/sev.py deleted file mode 100644 index 4efef4a9..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/sev/sev.py +++ /dev/null @@ -1,109 +0,0 @@ -import base64 -import ctypes -import logging -import os -from typing import Optional -from nilai_common import Nonce, AMDAttestationToken - -logger = logging.getLogger(__name__) - - -class SEVGuest: - def __init__(self): - self.lib: Optional[ctypes.CDLL] = None - self._load_library() - - def _load_library(self) -> None: - try: - lib_path = f"{os.path.dirname(os.path.abspath(__file__))}/libsevguest.so" - if not os.path.exists(lib_path): - logger.warning(f"SEV library not found at {lib_path}") - return - - self.lib = ctypes.CDLL(lib_path) - self._setup_library_functions() - except Exception as e: - logger.warning(f"Failed to load SEV library: {e}") - self.lib = None - - def _setup_library_functions(self) -> None: - if not self.lib: - return - - self.lib.OpenDevice.restype = ctypes.c_int - self.lib.GetQuoteProvider.restype = ctypes.c_int - self.lib.Init.restype = ctypes.c_int - self.lib.GetQuote.restype = ctypes.c_char_p - self.lib.GetQuote.argtypes = [ctypes.c_char_p] - self.lib.VerifyQuote.restype = ctypes.c_int - self.lib.VerifyQuote.argtypes = [ctypes.c_char_p] - self.lib.free.argtypes = [ctypes.c_char_p] - - def init(self) -> bool: - """Initialize the device and quote provider.""" - if not self.lib: - logger.warning("SEV library not loaded, running in mock mode") - return True - if self.lib.Init() != 0: - self.lib = None - return False - return self.lib.Init() == 0 - - def get_quote(self, nonce: Optional[Nonce] = None) -> AMDAttestationToken: - """Get a quote using the report data.""" - if not self.lib: - logger.warning("SEV library not loaded, returning mock quote") - return base64.b64encode(b"mock_quote").decode("ascii") - - if nonce is None: - nonce = "0" * 64 - - if not isinstance(nonce, str): - raise ValueError("Nonce must be a string") - - if len(nonce) != 64: - raise ValueError("Nonce must be exactly 64 bytes") - - # Convert string nonce to bytes - nonce_bytes = nonce.encode("utf-8") - nonce_buffer = ctypes.create_string_buffer(nonce_bytes) - quote_ptr = self.lib.GetQuote(nonce_buffer) - - if quote_ptr is None: - raise RuntimeError("Failed to get quote") - - quote_str = ctypes.string_at(quote_ptr) - return base64.b64encode(quote_str).decode("ascii") - - def verify_quote(self, quote: str) -> bool: - """Verify the quote using the library's verification method.""" - if not self.lib: - logger.warning( - "SEV library not loaded, mock verification always returns True" - ) - return True - - quote_bytes = base64.b64decode(quote.encode("ascii")) - quote_buffer = ctypes.create_string_buffer(quote_bytes) - return self.lib.VerifyQuote(quote_buffer) == 0 - - -# Global instance -sev = SEVGuest() - -if __name__ == "__main__": - try: - if sev.init(): - print("SEV guest device initialized successfully.") - report_data: Nonce = "0" * 64 - quote = sev.get_quote(report_data) - print("Quote:", quote) - - if sev.verify_quote(quote): - print("Quote verified successfully.") - else: - print("Quote verification failed.") - else: - print("Failed to initialize SEV guest device.") - except Exception as e: - print("Error:", e) diff --git a/nilai-attestation/src/nilai_attestation/attestation/tdx/README.md b/nilai-attestation/src/nilai_attestation/attestation/tdx/README.md deleted file mode 100644 index 17df353f..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/tdx/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Intel TDX integration with NilAI - -To add the integration, we need the following: - -```shell -# Download required packages -go get -# Ensure dependencies are present -go mod tidy -``` - -```shell -go build -``` diff --git a/nilai-attestation/src/nilai_attestation/attestation/tdx/go.mod b/nilai-attestation/src/nilai_attestation/attestation/tdx/go.mod deleted file mode 100644 index 42b36867..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/tdx/go.mod +++ /dev/null @@ -1,16 +0,0 @@ -module nillion/tdx - -go 1.23.3 - -require github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 // Downloaded from git history at 2024-11-25 18:55 - -require ( - github.com/google/go-configfs-tsm v0.3.2 // indirect - github.com/google/go-eventlog v0.0.1 // indirect - github.com/google/go-tpm v0.9.0 // indirect - github.com/google/logger v1.1.1 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sys v0.19.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect -) diff --git a/nilai-attestation/src/nilai_attestation/attestation/tdx/go.sum b/nilai-attestation/src/nilai_attestation/attestation/tdx/go.sum deleted file mode 100644 index 28c6dd49..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/tdx/go.sum +++ /dev/null @@ -1,29 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-configfs-tsm v0.3.2 h1:ZYmHkdQavfsvVGDtX7RRda0gamelUNUhu0A9fbiuLmE= -github.com/google/go-configfs-tsm v0.3.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= -github.com/google/go-eventlog v0.0.1 h1:7lV3gf61LNDhfS9gQplqaJc/j9ztLhKKgZk/lR6vv4Q= -github.com/google/go-eventlog v0.0.1/go.mod h1:7huE5P8w2NTObSwSJjboHmB7ioBNblkijdzoVa2skfQ= -github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 h1:+MoPobRN9HrDhGyn6HnF5NYo4uMBKaiFqAtf/D/OB4A= -github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= -github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= -github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= -github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= -github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/nilai-attestation/src/nilai_attestation/attestation/tdx/main.go b/nilai-attestation/src/nilai_attestation/attestation/tdx/main.go deleted file mode 100644 index d20dacdd..00000000 --- a/nilai-attestation/src/nilai_attestation/attestation/tdx/main.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/google/go-tdx-guest/client" - "github.com/google/go-tdx-guest/verify" - - "github.com/google/go-tdx-guest/rtmr" -) - -var device client.Device -var quoteProvider client.QuoteProvider - -func main() { - // Choose a mock device or a real device depending on the --tdx_guest_device_path flag. - var err error - - if device, err = client.OpenDevice(); err != nil { - panic(fmt.Sprintf("failed to open device: %v", err)) - } - - if quoteProvider, err = client.GetQuoteProvider(); err != nil { - panic(fmt.Sprintf("failed to get quote provider: %v", err)) - } - - // Use the device to get a quote. - reportData := [64]byte{0} - quote, err := client.GetQuote(quoteProvider, reportData) - if err != nil { - panic(fmt.Sprintf("failed to get raw quote: %v", err)) - } - - // Close the device. - if err := device.Close(); err != nil { - panic(fmt.Sprintf("failed to close device: %v", err)) - } - - // Verify the quote. - err = verify.TdxQuote(quote, &verify.Options{}) - if err != nil { - panic(fmt.Sprintf("failed to verify quote: %v", err)) - } - - // This is a mock digest. - // It should be the docker image hash. - digest := [64]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} - // Extend the digest to the rtmr 3 - rtmr.ExtendDigest(3, digest[:]) - - // Get the RTM report. - rtmReport, err := rtmr.GetRtmrsFromTdQuote(quote) - if err != nil { - panic(fmt.Sprintf("failed to get RTM report: %v", err)) - } - - _ = rtmReport - -} diff --git a/nilai-attestation/src/nilai_attestation/routers/__init__.py b/nilai-attestation/src/nilai_attestation/routers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/nilai-attestation/src/nilai_attestation/routers/private.py b/nilai-attestation/src/nilai_attestation/routers/private.py deleted file mode 100644 index 90de1802..00000000 --- a/nilai-attestation/src/nilai_attestation/routers/private.py +++ /dev/null @@ -1,48 +0,0 @@ -# Fast API and serving -import logging -from fastapi import APIRouter, Depends - -# Internal libraries -from nilai_attestation.attestation import ( - get_attestation_report, - verify_attestation_report, -) -from nilai_common import ( - AttestationReport, - Nonce, -) - -logger = logging.getLogger(__name__) - -router = APIRouter() - - -@router.get("/attestation/report", tags=["Attestation"]) -async def get_attestation(nonce: Nonce | None = None) -> AttestationReport: - """ - Generate a cryptographic attestation report. - - - **nonce**: Optional nonce for the attestation (query parameter) - - **Returns**: Attestation details for service verification - - ### Attestation Details - - `cpu_attestation`: CPU environment verification - - `gpu_attestation`: GPU environment verification - - ### Security Note - Provides cryptographic proof of the service's integrity and environment. - """ - return get_attestation_report(nonce) - - -@router.get("/attestation/verify", tags=["Attestation"]) -async def get_attestation_verification( - attestation_report: AttestationReport = Depends(), -) -> bool: - """ - Verify a cryptographic attestation report passed as query parameters. - - - **attestation_report**: Attestation report to verify (fields passed as query parameters) - - **Returns**: True if the attestation report is valid, False otherwise - """ - return verify_attestation_report(attestation_report) diff --git a/nilai-attestation/src/nilai_attestation/routers/public.py b/nilai-attestation/src/nilai_attestation/routers/public.py deleted file mode 100644 index c44c74d2..00000000 --- a/nilai-attestation/src/nilai_attestation/routers/public.py +++ /dev/null @@ -1,34 +0,0 @@ -# Fast API and serving -from fastapi import APIRouter - -# Internal libraries -from nilai_common import HealthCheckResponse - -router = APIRouter() - - -# Health Check Endpoint -@router.get("/health", tags=["Health"]) -async def health_check() -> HealthCheckResponse: - """ - Perform a system health check. - - - **Returns**: Current system health status and uptime - - ### Health Check Details - - Provides a quick verification of system operational status - - Reports current system uptime - - ### Status Indicators - - `status`: Indicates system operational condition - - `"ok"`: System is functioning normally - - `uptime`: Duration the system has been running - - ### Example - ```python - # Retrieve system health status - health = await health_check() - # Expect: HealthCheckResponse(status='ok', uptime=3600) - ``` - """ - return HealthCheckResponse(status="ok", uptime="") diff --git a/nilai-attestation/tests/__init__.py b/nilai-attestation/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/nilai-attestation/tests/sev/__init__.py b/nilai-attestation/tests/sev/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/nilai-attestation/tests/sev/test_sev.py b/nilai-attestation/tests/sev/test_sev.py deleted file mode 100644 index 012a3ab8..00000000 --- a/nilai-attestation/tests/sev/test_sev.py +++ /dev/null @@ -1,65 +0,0 @@ -import base64 -import ctypes - -import pytest -from nilai_attestation.attestation.sev.sev import SEVGuest # type: ignore - - -@pytest.fixture -def sev_guest(): - return SEVGuest() - - -def test_init_success(sev_guest, mocker): - mocker.patch.object(sev_guest, "_load_library", return_value=None) - sev_guest.lib = mocker.Mock() - sev_guest.lib.Init.return_value = 0 - assert sev_guest.init() is True - - -def test_init_failure(sev_guest, mocker): - mocker.patch.object(sev_guest, "_load_library", return_value=None) - sev_guest.lib = mocker.Mock() - sev_guest.lib.Init.return_value = -1 - assert sev_guest.init() is False - - -def test_get_quote_success(sev_guest, mocker): - mocker.patch.object(sev_guest, "_load_library", return_value=None) - sev_guest.lib = mocker.Mock() - sev_guest.lib.GetQuote.return_value = ctypes.create_string_buffer(b"quote_data") - report_data = bytes([0] * 64) - quote = sev_guest.get_quote(report_data) - expected_quote = base64.b64encode(b"quote_data").decode("ascii") - assert quote == expected_quote - - -def test_get_quote_failure(sev_guest, mocker): - mocker.patch.object(sev_guest, "_load_library", return_value=None) - sev_guest.lib = mocker.Mock() - sev_guest.lib.GetQuote.return_value = None - report_data = bytes([0] * 64) - with pytest.raises(RuntimeError): - sev_guest.get_quote(report_data) - - -def test_get_quote_invalid_report_data(sev_guest): - if sev_guest.lib is not None: - with pytest.raises(ValueError): - sev_guest.get_quote(bytes([0] * 63)) - - -def test_verify_quote_success(sev_guest, mocker): - mocker.patch.object(sev_guest, "_load_library", return_value=None) - sev_guest.lib = mocker.Mock() - sev_guest.lib.VerifyQuote.return_value = 0 - quote = base64.b64encode(b"quote_data").decode("ascii") - assert sev_guest.verify_quote(quote) is True - - -def test_verify_quote_failure(sev_guest, mocker): - mocker.patch.object(sev_guest, "_load_library", return_value=None) - sev_guest.lib = mocker.Mock() - sev_guest.lib.VerifyQuote.return_value = -1 - quote = base64.b64encode(b"quote_data").decode("ascii") - assert sev_guest.verify_quote(quote) is False diff --git a/nilai-attestation/uv.lock b/nilai-attestation/uv.lock deleted file mode 100644 index d2f00389..00000000 --- a/nilai-attestation/uv.lock +++ /dev/null @@ -1,1061 +0,0 @@ -version = 1 -revision = 1 -requires-python = "==3.12.*" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, -] - -[[package]] -name = "astroid" -version = "3.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/33/536530122a22a7504b159bccaf30a1f76aa19d23028bd8b5009eb9b2efea/astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550", size = 398731 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/80/c749efbd8eef5ea77c7d6f1956e8fbfb51963b7f93ef79647afd4d9886e3/astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248", size = 275339 }, -] - -[[package]] -name = "build" -version = "1.2.2.post1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "os_name == 'nt'" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950 }, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "coverage" -version = "7.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, - { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, - { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, - { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, - { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, - { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, - { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, - { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, - { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, - { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, - { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, -] - -[[package]] -name = "cryptography" -version = "43.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/ba/0664727028b37e249e73879348cc46d45c5c1a2a2e81e8166462953c5755/cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", size = 686927 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/28/b92c98a04ba762f8cdeb54eba5c4c84e63cac037a7c5e70117d337b15ad6/cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", size = 6223222 }, - { url = "https://files.pythonhosted.org/packages/33/13/1193774705783ba364121aa2a60132fa31a668b8ababd5edfa1662354ccd/cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", size = 3794751 }, - { url = "https://files.pythonhosted.org/packages/5e/4b/39bb3c4c8cfb3e94e736b8d8859ce5c81536e91a1033b1d26770c4249000/cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", size = 3981827 }, - { url = "https://files.pythonhosted.org/packages/ce/dc/1471d4d56608e1013237af334b8a4c35d53895694fbb73882d1c4fd3f55e/cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", size = 3780034 }, - { url = "https://files.pythonhosted.org/packages/ad/43/7a9920135b0d5437cc2f8f529fa757431eb6a7736ddfadfdee1cc5890800/cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", size = 3993407 }, - { url = "https://files.pythonhosted.org/packages/cc/42/9ab8467af6c0b76f3d9b8f01d1cf25b9c9f3f2151f4acfab888d21c55a72/cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", size = 3886457 }, - { url = "https://files.pythonhosted.org/packages/a4/65/430509e31700286ec02868a2457d2111d03ccefc20349d24e58d171ae0a7/cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", size = 4081499 }, - { url = "https://files.pythonhosted.org/packages/bb/18/a04b6467e6e09df8c73b91dcee8878f4a438a43a3603dc3cd6f8003b92d8/cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", size = 2616504 }, - { url = "https://files.pythonhosted.org/packages/cc/73/0eacbdc437202edcbdc07f3576ed8fb8b0ab79d27bf2c5d822d758a72faa/cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", size = 3067456 }, - { url = "https://files.pythonhosted.org/packages/8a/b6/bc54b371f02cffd35ff8dc6baba88304d7cf8e83632566b4b42e00383e03/cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", size = 6225263 }, - { url = "https://files.pythonhosted.org/packages/00/0e/8217e348a1fa417ec4c78cd3cdf24154f5e76fd7597343a35bd403650dfd/cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", size = 3794368 }, - { url = "https://files.pythonhosted.org/packages/3d/ed/38b6be7254d8f7251fde8054af597ee8afa14f911da67a9410a45f602fc3/cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", size = 3981750 }, - { url = "https://files.pythonhosted.org/packages/64/f3/b7946c3887cf7436f002f4cbb1e6aec77b8d299b86be48eeadfefb937c4b/cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", size = 3778925 }, - { url = "https://files.pythonhosted.org/packages/ac/7e/ebda4dd4ae098a0990753efbb4b50954f1d03003846b943ea85070782da7/cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", size = 3993152 }, - { url = "https://files.pythonhosted.org/packages/43/f6/feebbd78a3e341e3913846a3bb2c29d0b09b1b3af1573c6baabc2533e147/cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", size = 3886392 }, - { url = "https://files.pythonhosted.org/packages/bd/4c/ab0b9407d5247576290b4fd8abd06b7f51bd414f04eef0f2800675512d61/cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", size = 4082606 }, - { url = "https://files.pythonhosted.org/packages/05/36/e532a671998d6fcfdb9122da16434347a58a6bae9465e527e450e0bc60a5/cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", size = 2617948 }, - { url = "https://files.pythonhosted.org/packages/b3/c6/c09cee6968add5ff868525c3815e5dccc0e3c6e89eec58dc9135d3c40e88/cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", size = 3070445 }, -] - -[[package]] -name = "debtcollector" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/e2/a45b5a620145937529c840df5e499c267997e85de40df27d54424a158d3c/debtcollector-3.0.0.tar.gz", hash = "sha256:2a8917d25b0e1f1d0d365d3c1c6ecfc7a522b1e9716e8a1a4a915126f7ccea6f", size = 31322 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/ca/863ed8fa66d6f986de6ad7feccc5df96e37400845b1eeb29889a70feea99/debtcollector-3.0.0-py3-none-any.whl", hash = "sha256:46f9dacbe8ce49c47ebf2bf2ec878d50c9443dfae97cc7b8054be684e54c3e91", size = 23035 }, -] - -[[package]] -name = "dill" -version = "0.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/43/86fe3f9e130c4137b0f1b50784dd70a5087b911fe07fa81e53e0c4c47fea/dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c", size = 187000 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a", size = 119418 }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, -] - -[[package]] -name = "docutils" -version = "0.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, -] - -[[package]] -name = "ecdsa" -version = "0.18.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/7b/ba6547a76c468a0d22de93e89ae60d9561ec911f59532907e72b0d8bc0f1/ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49", size = 197938 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/d4/4f05f5d16a4863b30ba96c23b23e942da8889abfa1cdbabf2a0df12a4532/ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd", size = 142915 }, -] - -[[package]] -name = "elementpath" -version = "4.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/41/afdd82534c80e9675d1c51dc21d0889b72d023bfe395a2f5a44d751d3a73/elementpath-4.8.0.tar.gz", hash = "sha256:5822a2560d99e2633d95f78694c7ff9646adaa187db520da200a8e9479dc46ae", size = 358528 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/95/615af832e7f507fe5ce4562b4be1bd2fec080c4ff6da88dcd0c2dbfca582/elementpath-4.8.0-py3-none-any.whl", hash = "sha256:5393191f84969bcf8033b05ec4593ef940e58622ea13cefe60ecefbbf09d58d9", size = 243271 }, -] - -[[package]] -name = "etcd3gw" -version = "2.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "futurist" }, - { name = "pbr" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/56/db0e19678af91d9213cf21c72e7d82a3494d6fc7da16d61c6ba578fd8648/etcd3gw-2.4.2.tar.gz", hash = "sha256:6c6e9e42b810ee9a9455dd342de989f1fab637a94daa4fc34cacb248a54473fa", size = 29840 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/11/79f09e0d1195d455bdf0542d4fec4ddc80a4f496d090244bba9fc7113834/etcd3gw-2.4.2-py3-none-any.whl", hash = "sha256:b907bd2dc702eabbeba3f9c15666e94e92961bfe685429a0e415ce44097f5c22", size = 24092 }, -] - -[[package]] -name = "fastapi" -version = "0.115.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, -] - -[[package]] -name = "futurist" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "debtcollector" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/5f/b85f54ef457d154b1ae516fac1f09377323aef65bd12903f94a625345534/futurist-3.1.1.tar.gz", hash = "sha256:cc95dd9a40923848e32157128eb7a14b78ef32507b1ef82284ecbe1c373feee2", size = 45177 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/2f/083d0e43dcb18d07002fd7d124d8aa3a32b1935d7664189505311f227c68/futurist-3.1.1-py3-none-any.whl", hash = "sha256:82f77eb5154670ca0ebbcaa9e92b55c03cdb5d2e34c6eb3746ca7eddcbe87a37", size = 37100 }, -] - -[[package]] -name = "gunicorn" -version = "23.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, -] - -[[package]] -name = "id" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, -] - -[[package]] -name = "isort" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186 }, -] - -[[package]] -name = "jaraco-classes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, -] - -[[package]] -name = "jaraco-context" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, -] - -[[package]] -name = "jaraco-functools" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, -] - -[[package]] -name = "jeepney" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, -] - -[[package]] -name = "jiter" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/d7/c55086103d6f29b694ec79156242304adf521577530d9031317ce5338c59/jiter-0.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7b46249cfd6c48da28f89eb0be3f52d6fdb40ab88e2c66804f546674e539ec11", size = 309203 }, - { url = "https://files.pythonhosted.org/packages/b0/01/f775dfee50beb420adfd6baf58d1c4d437de41c9b666ddf127c065e5a488/jiter-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:609cf3c78852f1189894383cf0b0b977665f54cb38788e3e6b941fa6d982c00e", size = 319678 }, - { url = "https://files.pythonhosted.org/packages/ab/b8/09b73a793714726893e5d46d5c534a63709261af3d24444ad07885ce87cb/jiter-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d726a3890a54561e55a9c5faea1f7655eda7f105bd165067575ace6e65f80bb2", size = 341816 }, - { url = "https://files.pythonhosted.org/packages/35/6f/b8f89ec5398b2b0d344257138182cc090302854ed63ed9c9051e9c673441/jiter-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e89dc075c1fef8fa9be219e249f14040270dbc507df4215c324a1839522ea75", size = 364152 }, - { url = "https://files.pythonhosted.org/packages/9b/ca/978cc3183113b8e4484cc7e210a9ad3c6614396e7abd5407ea8aa1458eef/jiter-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04e8ffa3c353b1bc4134f96f167a2082494351e42888dfcf06e944f2729cbe1d", size = 406991 }, - { url = "https://files.pythonhosted.org/packages/13/3a/72861883e11a36d6aa314b4922125f6ae90bdccc225cd96d24cc78a66385/jiter-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:203f28a72a05ae0e129b3ed1f75f56bc419d5f91dfacd057519a8bd137b00c42", size = 395824 }, - { url = "https://files.pythonhosted.org/packages/87/67/22728a86ef53589c3720225778f7c5fdb617080e3deaed58b04789418212/jiter-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca1a02ad60ec30bb230f65bc01f611c8608b02d269f998bc29cca8619a919dc", size = 351318 }, - { url = "https://files.pythonhosted.org/packages/69/b9/f39728e2e2007276806d7a6609cda7fac44ffa28ca0d02c49a4f397cc0d9/jiter-0.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:237e5cee4d5d2659aaf91bbf8ec45052cc217d9446070699441a91b386ae27dc", size = 384591 }, - { url = "https://files.pythonhosted.org/packages/eb/8f/8a708bc7fd87b8a5d861f1c118a995eccbe6d672fe10c9753e67362d0dd0/jiter-0.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:528b6b71745e7326eed73c53d4aa57e2a522242320b6f7d65b9c5af83cf49b6e", size = 520746 }, - { url = "https://files.pythonhosted.org/packages/95/1e/65680c7488bd2365dbd2980adaf63c562d3d41d3faac192ebc7ef5b4ae25/jiter-0.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9f48e86b57bc711eb5acdfd12b6cb580a59cc9a993f6e7dcb6d8b50522dcd50d", size = 512754 }, - { url = "https://files.pythonhosted.org/packages/78/f3/fdc43547a9ee6e93c837685da704fb6da7dba311fc022e2766d5277dfde5/jiter-0.9.0-cp312-cp312-win32.whl", hash = "sha256:699edfde481e191d81f9cf6d2211debbfe4bd92f06410e7637dffb8dd5dfde06", size = 207075 }, - { url = "https://files.pythonhosted.org/packages/cd/9d/742b289016d155f49028fe1bfbeb935c9bf0ffeefdf77daf4a63a42bb72b/jiter-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:099500d07b43f61d8bd780466d429c45a7b25411b334c60ca875fa775f68ccb0", size = 207999 }, -] - -[[package]] -name = "keyring" -version = "25.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "secretstorage", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085 }, -] - -[[package]] -name = "lxml" -version = "4.9.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/14/c2070b5e37c650198de8328467dd3d1681e80986f81ba0fea04fc4ec9883/lxml-4.9.4.tar.gz", hash = "sha256:b1541e50b78e15fa06a2670157a1962ef06591d4c998b998047fff5e3236880e", size = 3576664 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/ac/0abe4b25cae50247c5130539d0f45a201dbfe0ba69d3dd844411f90c9930/lxml-4.9.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:dbcb2dc07308453db428a95a4d03259bd8caea97d7f0776842299f2d00c72fc8", size = 8624172 }, - { url = "https://files.pythonhosted.org/packages/33/e6/47c4675f0c58398c924915379eee8458bf7954644a7907ad8fbc1c42a380/lxml-4.9.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:01bf1df1db327e748dcb152d17389cf6d0a8c5d533ef9bab781e9d5037619229", size = 7674086 }, - { url = "https://files.pythonhosted.org/packages/be/9e/5d88b189e91fae65140dc29904946297b3d9cfdf5449d4bc6e657a3ffc2d/lxml-4.9.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e8f9f93a23634cfafbad6e46ad7d09e0f4a25a2400e4a64b1b7b7c0fbaa06d9d", size = 8026189 }, - { url = "https://files.pythonhosted.org/packages/ea/08/ab6c2a803a5d5dce1fbbb32f5c133bbd0ebfe69476ab1eb5ffa3490b0b51/lxml-4.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3f3f00a9061605725df1816f5713d10cd94636347ed651abdbc75828df302b20", size = 7516933 }, - { url = "https://files.pythonhosted.org/packages/43/52/b0d387577620af767c73b8b20f28986e5aad70b44053ee296f8a472a12b1/lxml-4.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:953dd5481bd6252bd480d6ec431f61d7d87fdcbbb71b0d2bdcfc6ae00bb6fb10", size = 7815609 }, - { url = "https://files.pythonhosted.org/packages/be/13/18230c0d567ed282a3d7b61395323e2ef8fc9ad64096fdd3d1b384fa3e3c/lxml-4.9.4-cp312-cp312-win32.whl", hash = "sha256:266f655d1baff9c47b52f529b5f6bec33f66042f65f7c56adde3fcf2ed62ae8b", size = 3460500 }, - { url = "https://files.pythonhosted.org/packages/5f/df/6d15cc415e04724ba4c141051cf43709e09bbcdd9868a6c2e7a7073ef498/lxml-4.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:f1faee2a831fe249e1bae9cbc68d3cd8a30f7e37851deee4d7962b17c410dd56", size = 3773977 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "more-itertools" -version = "10.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/3b/7fa1fe835e2e93fd6d7b52b2f95ae810cf5ba133e1845f726f5a992d62c2/more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", size = 125009 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/62/0fe302c6d1be1c777cab0616e6302478251dfbf9055ad426f5d0def75c89/more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89", size = 63038 }, -] - -[[package]] -name = "nh3" -version = "0.2.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133 }, - { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328 }, - { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020 }, - { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878 }, - { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460 }, - { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369 }, - { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036 }, - { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712 }, - { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559 }, - { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591 }, - { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670 }, - { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093 }, - { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623 }, - { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283 }, -] - -[[package]] -name = "nilai-attestation" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "fastapi" }, - { name = "gunicorn" }, - { name = "nilai-common" }, - { name = "nv-attestation-sdk" }, - { name = "uvicorn" }, -] - -[package.metadata] -requires-dist = [ - { name = "fastapi", specifier = ">=0.115.12" }, - { name = "gunicorn", specifier = ">=23.0.0" }, - { name = "nilai-common", editable = "../packages/nilai-common" }, - { name = "nv-attestation-sdk", specifier = "==2.4.0" }, - { name = "uvicorn", specifier = ">=0.34.0" }, -] - -[[package]] -name = "nilai-common" -version = "0.1.0" -source = { editable = "../packages/nilai-common" } -dependencies = [ - { name = "etcd3gw" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "tenacity" }, -] - -[package.metadata] -requires-dist = [ - { name = "etcd3gw", specifier = ">=2.4.2" }, - { name = "openai", specifier = ">=1.59.9" }, - { name = "pydantic", specifier = ">=2.10.1" }, - { name = "tenacity", specifier = ">=9.0.0" }, -] - -[[package]] -name = "nv-attestation-sdk" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "build" }, - { name = "cryptography" }, - { name = "ecdsa" }, - { name = "nv-local-gpu-verifier" }, - { name = "nvidia-ml-py" }, - { name = "pyjwt" }, - { name = "pylint" }, - { name = "pyopenssl" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "requests" }, - { name = "signxml" }, - { name = "twine" }, - { name = "xmlschema" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/16/1f176c805f74b85a71f509fb13a7fc06ea3400316c558597f6e2c926989d/nv_attestation_sdk-2.4.0-py3-none-any.whl", hash = "sha256:f7de18ad473ea8a58e41f94ac2067b5c4ba1e051234b5f274b48c11280c5f0ae", size = 97939 }, -] - -[[package]] -name = "nv-local-gpu-verifier" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "ecdsa" }, - { name = "lxml" }, - { name = "nvidia-ml-py" }, - { name = "pyjwt" }, - { name = "pyopenssl" }, - { name = "signxml" }, - { name = "xmlschema" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/3b/36d757f361a18d439d5be4f59cd14053d8a3ea7e398d46da14280faefbc2/nv_local_gpu_verifier-2.4.0-py3-none-any.whl", hash = "sha256:7c02b83bb4181f3307fc43b30d64ebfea8af8e4e12d9e143a7041dfa627b40a3", size = 210048 }, -] - -[[package]] -name = "nvidia-ml-py" -version = "12.550.52" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/f0/7a123ecef9994f4551820e96575475df25bdb8038904723f7ea6de943234/nvidia-ml-py-12.550.52.tar.gz", hash = "sha256:dfedd714335c72e65a32c86e9f5db1cd49526d44d6d8c72809d996958f734c07", size = 37971 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/fb/4abda63f347daa50fcbf068ebfe37e10e247565af5df8473ddb7b3836ba4/nvidia_ml_py-12.550.52-py3-none-any.whl", hash = "sha256:b78a1175f299f702dea17fc468676443f3fefade880202da8d0997df15dc11e2", size = 39295 }, -] - -[[package]] -name = "openai" -version = "1.72.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/56/41de36c0e9f787c406211552ecf2ca4fba3db900207c5c158c4dc67263fc/openai-1.72.0.tar.gz", hash = "sha256:f51de971448905cc90ed5175a5b19e92fd94e31f68cde4025762f9f5257150db", size = 426061 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/1c/a0870f31bd71244c8c3a82e171677d9a148a8ea1cb157308cb9e06a41a37/openai-1.72.0-py3-none-any.whl", hash = "sha256:34f5496ba5c8cb06c592831d69e847e2d164526a2fb92afdc3b5cf2891c328c3", size = 643863 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[package]] -name = "pbr" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997 }, -] - -[[package]] -name = "platformdirs" -version = "4.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - -[[package]] -name = "pydantic" -version = "2.11.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, -] - -[[package]] -name = "pydantic-core" -version = "2.33.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, - { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, - { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, - { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, - { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, - { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, - { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, - { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, - { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, - { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, - { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, - { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, - { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, - { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, -] - -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, -] - -[[package]] -name = "pyjwt" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/f0/9804c72e9a314360c135f42c434eb42eaabb5e7ebad760cbd8fc7023be38/PyJWT-2.7.0.tar.gz", hash = "sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074", size = 77902 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/e8/01b2e35d81e618a8212e651e10c91660bdfda49c1d15ce66f4ca1ff43649/PyJWT-2.7.0-py3-none-any.whl", hash = "sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1", size = 22366 }, -] - -[[package]] -name = "pylint" -version = "3.3.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "astroid" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "dill" }, - { name = "isort" }, - { name = "mccabe" }, - { name = "platformdirs" }, - { name = "tomlkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/a7/113d02340afb9dcbb0c8b25454e9538cd08f0ebf3e510df4ed916caa1a89/pylint-3.3.6.tar.gz", hash = "sha256:b634a041aac33706d56a0d217e6587228c66427e20ec21a019bc4cdee48c040a", size = 1519586 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/21/9537fc94aee9ec7316a230a49895266cf02d78aa29b0a2efbc39566e0935/pylint-3.3.6-py3-none-any.whl", hash = "sha256:8b7c2d3e86ae3f94fb27703d521dd0b9b6b378775991f504d7c3a6275aa0a6a6", size = 522462 }, -] - -[[package]] -name = "pyopenssl" -version = "24.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/70/ff56a63248562e77c0c8ee4aefc3224258f1856977e0c1472672b62dadb8/pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95", size = 184323 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/dd/e0aa7ebef5168c75b772eda64978c597a9129b46be17779054652a7999e4/pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d", size = 58390 }, -] - -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 }, -] - -[[package]] -name = "pytest" -version = "8.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/b7/7d44bbc04c531dcc753056920e0988032e5871ac674b5a84cb979de6e7af/pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044", size = 1409703 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/7e/c79cecfdb6aa85c6c2e3cf63afc56d0f165f24f5c66c03c695c4d9b84756/pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", size = 337359 }, -] - -[[package]] -name = "pytest-cov" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, -] - -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, -] - -[[package]] -name = "readme-renderer" -version = "44.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "nh3" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, -] - -[[package]] -name = "rfc3986" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, -] - -[[package]] -name = "rich" -version = "14.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, -] - -[[package]] -name = "secretstorage" -version = "3.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, -] - -[[package]] -name = "setuptools" -version = "78.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, -] - -[[package]] -name = "signxml" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "cryptography" }, - { name = "lxml" }, - { name = "pyopenssl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/f3/6910019f60efba3d76e42540013cb27203a41eaa9bc2ec9edbb2e0d7623f/signxml-3.2.0.tar.gz", hash = "sha256:da4a85c272998bb3a18211f9e21cbfe1359b756706bc4bddbeb4020babdab7ef", size = 58650 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/4b/1e3f14db5967ae48064cdff7683f52e0c492c6d98a40285a6a4bf449149f/signxml-3.2.0-py3-none-any.whl", hash = "sha256:0ee07e3e8fcba8fa0975f5bf9e205e557ea3f0b34ea95b4fde1c897e75c4812c", size = 57867 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "starlette" -version = "0.46.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, -] - -[[package]] -name = "tenacity" -version = "9.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, -] - -[[package]] -name = "tomlkit" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, -] - -[[package]] -name = "twine" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "id" }, - { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, - { name = "packaging" }, - { name = "readme-renderer" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "rfc3986" }, - { name = "rich" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791 }, -] - -[[package]] -name = "typing-extensions" -version = "4.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, -] - -[[package]] -name = "uvicorn" -version = "0.34.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, -] - -[[package]] -name = "wrapt" -version = "1.17.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, - { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, - { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, - { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, - { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, - { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, - { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, - { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, - { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, - { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, - { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, -] - -[[package]] -name = "xmlschema" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "elementpath" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/ff/3aaa6bf60779599427ebdb905d66d16377bcdef98d0b91b9619758069c78/xmlschema-2.2.3.tar.gz", hash = "sha256:d21ba86af4432720231fb4b40f1205fa75fd718d6856ec3b8118984de31c225b", size = 493444 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/32/aac4ca0f985a7d5e28ba8b0a90c50868b2dafa2f263f4e49e1bb852f7d95/xmlschema-2.2.3-py3-none-any.whl", hash = "sha256:7d971045eeeb8de183b56bc7530eb8f3d8276072d08017a962c2c34e93bfdd26", size = 355468 }, -] diff --git a/nilai-models/src/nilai_models/daemon.py b/nilai-models/src/nilai_models/daemon.py index bdb883c2..d486ff7e 100644 --- a/nilai-models/src/nilai_models/daemon.py +++ b/nilai-models/src/nilai_models/daemon.py @@ -6,6 +6,7 @@ import httpx from nilai_common import ( + MODEL_CAPABILITIES, MODEL_SETTINGS, SETTINGS, ModelEndpoint, @@ -37,8 +38,8 @@ async def get_metadata(): license="Apache 2.0", source=f"https://huggingface.co/{model_name}", supported_features=["chat_completion"], - tool_support=SETTINGS.tool_support, - multimodal_support=SETTINGS.multimodal_support, + tool_support=MODEL_CAPABILITIES.tool_support, + multimodal_support=MODEL_CAPABILITIES.multimodal_support, ) except Exception as e: @@ -57,13 +58,13 @@ async def get_metadata(): async def run_service(discovery_service, model_endpoint): """Register model with discovery service and keep it alive.""" - lease = None + key = None try: logger.info(f"Registering model: {model_endpoint.metadata.id}") - lease = await discovery_service.register_model(model_endpoint, prefix="/models") + key = await discovery_service.register_model(model_endpoint, prefix="/models") logger.info(f"Model registered successfully: {model_endpoint}") - await discovery_service.keep_alive(lease) + await discovery_service.keep_alive(key, model_endpoint) except asyncio.CancelledError: logger.info("Service shutdown requested") @@ -72,13 +73,16 @@ async def run_service(discovery_service, model_endpoint): logger.error(f"Service error: {e}") raise finally: - if lease: + if key: try: await discovery_service.unregister_model(model_endpoint.metadata.id) logger.info(f"Model unregistered: {model_endpoint.metadata.id}") except Exception as e: logger.error(f"Error unregistering model: {e}") + # Close the discovery service connection + await discovery_service.close() + async def main(): """Main entry point for model daemon.""" @@ -86,8 +90,9 @@ async def main(): # Initialize discovery service discovery_service = ModelServiceDiscovery( - host=SETTINGS.etcd_host, port=SETTINGS.etcd_port + host=SETTINGS.discovery_host, port=SETTINGS.discovery_port ) + await discovery_service.initialize() # Fetch metadata and create endpoint metadata = await get_metadata() diff --git a/nilai-models/src/nilai_models/lmstudio_announcer.py b/nilai-models/src/nilai_models/lmstudio_announcer.py index ce2740a7..a8e83b3f 100644 --- a/nilai-models/src/nilai_models/lmstudio_announcer.py +++ b/nilai-models/src/nilai_models/lmstudio_announcer.py @@ -76,38 +76,43 @@ async def _fetch_model_ids( async def _announce_model( metadata: ModelMetadata, base_url: str, - etcd_host: str, - etcd_port: int, + discovery_host: str, + discovery_port: int, lease_ttl: int, prefix: str, ): - """Register and maintain a model announcement in etcd.""" + """Register and maintain a model announcement in Redis.""" discovery = ModelServiceDiscovery( - host=etcd_host, port=etcd_port, lease_ttl=lease_ttl + host=discovery_host, port=discovery_port, lease_ttl=lease_ttl ) + await discovery.initialize() + endpoint = ModelEndpoint(url=base_url.rstrip("/"), metadata=metadata) - lease = None + key = None try: - lease = await discovery.register_model(endpoint, prefix=prefix) + key = await discovery.register_model(endpoint, prefix=prefix) logger.info( - "Registered model %s at %s (lease=%s)", + "Registered model %s at %s (key=%s)", metadata.id, endpoint.url, - getattr(lease, "id", "n/a"), + key, ) - await discovery.keep_alive(lease) + await discovery.keep_alive(key, endpoint) except asyncio.CancelledError: logger.info("Shutdown requested for model %s", metadata.id) raise finally: - if lease: + if key: try: await discovery.unregister_model(metadata.id) logger.info("Unregistered model %s", metadata.id) except Exception as exc: logger.error("Failed to unregister model %s: %s", metadata.id, exc) + # Close the discovery service connection + await discovery.close() + def _create_metadata( model_id: str, @@ -172,10 +177,10 @@ async def main(): os.getenv("LMSTUDIO_SUPPORTED_FEATURES", "chat_completion") ) or ["chat_completion"] - tool_default = to_bool(os.getenv("LMSTUDIO_TOOL_SUPPORT_DEFAULT", "false")) + tool_default = to_bool(os.getenv("LMSTUDIO_TOOL_SUPPORT_DEFAULT", "true")) tool_models = set(_parse_csv(os.getenv("LMSTUDIO_TOOL_SUPPORT_MODELS", ""))) - multimodal_default = to_bool(os.getenv("LMSTUDIO_MULTIMODAL_DEFAULT", "false")) + multimodal_default = to_bool(os.getenv("LMSTUDIO_MULTIMODAL_DEFAULT", "true")) multimodal_models = set(_parse_csv(os.getenv("LMSTUDIO_MULTIMODAL_MODELS", ""))) version = os.getenv("LMSTUDIO_MODEL_VERSION", "local") @@ -190,11 +195,11 @@ async def main(): ) logger.info( - "Announcing LMStudio models %s via %s with etcd at %s:%s", + "Announcing LMStudio models %s via %s with Redis at %s:%s", ", ".join(model_ids), registration_url, - SETTINGS.etcd_host, - SETTINGS.etcd_port, + SETTINGS.discovery_host, + SETTINGS.discovery_port, ) # Create announcement tasks for all models @@ -215,8 +220,8 @@ async def main(): multimodal_default, ), base_url=registration_url, - etcd_host=SETTINGS.etcd_host, - etcd_port=SETTINGS.etcd_port, + discovery_host=SETTINGS.discovery_host, + discovery_port=SETTINGS.discovery_port, lease_ttl=lease_ttl, prefix=discovery_prefix, ) diff --git a/vllm_templates/llama3.1_tool_json.jinja b/nilai-models/vllm_templates/llama3.1_tool_json.jinja similarity index 100% rename from vllm_templates/llama3.1_tool_json.jinja rename to nilai-models/vllm_templates/llama3.1_tool_json.jinja diff --git a/packages/nilai-common/pyproject.toml b/packages/nilai-common/pyproject.toml index 56b20bd1..d27f05f7 100644 --- a/packages/nilai-common/pyproject.toml +++ b/packages/nilai-common/pyproject.toml @@ -8,8 +8,8 @@ authors = [ ] requires-python = ">=3.12" dependencies = [ - "etcd3gw>=2.4.2", "openai>=1.99.2", + "redis>=5.0.0", "pydantic>=2.10.1", "tenacity>=9.0.0", ] diff --git a/packages/nilai-common/src/nilai_common/__init__.py b/packages/nilai-common/src/nilai_common/__init__.py index 385deb3b..574b7437 100644 --- a/packages/nilai-common/src/nilai_common/__init__.py +++ b/packages/nilai-common/src/nilai_common/__init__.py @@ -3,7 +3,6 @@ HealthCheckResponse, ModelEndpoint, ModelMetadata, - Nonce, AMDAttestationToken, NVAttestationToken, SearchResult, @@ -33,10 +32,10 @@ ResponseInputItemParam, EasyInputMessageParam, ResponseFunctionToolCallParam, + Usage, ) -from nilai_common.config import SETTINGS, MODEL_SETTINGS +from nilai_common.config import SETTINGS, MODEL_SETTINGS, MODEL_CAPABILITIES from nilai_common.discovery import ModelServiceDiscovery -from openai.types.completion_usage import CompletionUsage as Usage __all__ = [ "Message", @@ -49,16 +48,15 @@ "ChatCompletionMessageToolCall", "ChatToolFunction", "ModelMetadata", - "Usage", "AttestationReport", "HealthCheckResponse", "ModelEndpoint", "ModelServiceDiscovery", - "Nonce", "AMDAttestationToken", "NVAttestationToken", "SETTINGS", "MODEL_SETTINGS", + "MODEL_CAPABILITIES", "SearchResult", "Source", "TopicResponse", @@ -77,4 +75,5 @@ "ResponseInputItemParam", "EasyInputMessageParam", "ResponseFunctionToolCallParam", + "Usage", ] diff --git a/packages/nilai-common/src/nilai_common/api_models/__init__.py b/packages/nilai-common/src/nilai_common/api_models/__init__.py index 95243ff2..de76a353 100644 --- a/packages/nilai-common/src/nilai_common/api_models/__init__.py +++ b/packages/nilai-common/src/nilai_common/api_models/__init__.py @@ -30,6 +30,7 @@ MessageAdapter, ImageContent, TextContent, + Usage, ) from nilai_common.api_models.responses_model import ( @@ -74,6 +75,7 @@ "MessageAdapter", "ImageContent", "TextContent", + "Usage", "Response", "ResponseCompletedEvent", "ResponseRequest", diff --git a/packages/nilai-common/src/nilai_common/api_models/chat_completion_model.py b/packages/nilai-common/src/nilai_common/api_models/chat_completion_model.py index 1256279d..b34ce06e 100644 --- a/packages/nilai-common/src/nilai_common/api_models/chat_completion_model.py +++ b/packages/nilai-common/src/nilai_common/api_models/chat_completion_model.py @@ -1,6 +1,7 @@ -from __future__ import annotations +import uuid from typing import ( + Annotated, Iterable, List, Optional, @@ -29,6 +30,9 @@ from openai.types.chat.chat_completion_content_part_image_param import ( ChatCompletionContentPartImageParam, ) + +from openai.types.completion_usage import CompletionUsage as Usage + from openai.types.chat.chat_completion import Choice as OpenaAIChoice from pydantic import BaseModel, Field @@ -37,7 +41,40 @@ ChatToolFunction: TypeAlias = Function ImageContent: TypeAlias = ChatCompletionContentPartImageParam TextContent: TypeAlias = ChatCompletionContentPartTextParam -Message: TypeAlias = ChatCompletionMessageParam +Message: TypeAlias = ChatCompletionMessageParam # SDK union of message shapes + +# Explicitly re-export OpenAI types that are part of our public API +__all__ = [ + "ChatCompletion", + "ChatCompletionMessage", + "ChatCompletionMessageToolCall", + "ChatToolFunction", + "Function", + "ImageContent", + "TextContent", + "Message", + "ResultContent", + "Choice", + "Source", + "MessageAdapter", + "WebSearchEnhancedMessages", + "WebSearchContext", + "ChatRequest", + "SignedChatCompletion", + "ModelMetadata", + "ModelEndpoint", + "HealthCheckResponse", + "AttestationReport", + "AMDAttestationToken", + "NVAttestationToken", + "Usage", +] + + +# ---------- Domain-specific objects for web search ---------- +class ResultContent(BaseModel): + text: str + truncated: bool = False class Choice(OpenaAIChoice): @@ -285,3 +322,52 @@ class SignedChatCompletion(ChatCompletion): sources: Optional[List[Source]] = Field( default=None, description="Sources used for web search when enabled" ) + + +class ModelMetadata(BaseModel): + id: str = Field(default_factory=lambda: str(uuid.uuid4())) + name: str + version: str + description: str + author: str + license: str + source: str + supported_features: List[str] + tool_support: bool + multimodal_support: bool = False + + +class ModelEndpoint(BaseModel): + url: str + metadata: ModelMetadata + + +class HealthCheckResponse(BaseModel): + status: str + uptime: str + + +# ---------- Attestation ---------- +Nonce = Annotated[ + str, + Field( + max_length=64, + min_length=64, + description="The nonce to be used for the attestation", + ), +] + +AMDAttestationToken = Annotated[ + str, Field(description="The attestation token from AMD's attestation service") +] + +NVAttestationToken = Annotated[ + str, Field(description="The attestation token from NVIDIA's attestation service") +] + + +class AttestationReport(BaseModel): + nonce: Nonce + verifying_key: Annotated[str, Field(description="PEM encoded public key")] + cpu_attestation: AMDAttestationToken + gpu_attestation: NVAttestationToken diff --git a/packages/nilai-common/src/nilai_common/config.py b/packages/nilai-common/src/nilai_common/config.py deleted file mode 100644 index 294fc7b5..00000000 --- a/packages/nilai-common/src/nilai_common/config.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -from pydantic import BaseModel, Field - - -class HostSettings(BaseModel): - host: str = "localhost" - port: int = 8000 - etcd_host: str = "localhost" - etcd_port: int = 2379 - tool_support: bool = False - multimodal_support: bool = False - gunicorn_workers: int = 10 - attestation_host: str = "localhost" - attestation_port: int = 8081 - - -class ModelSettings(BaseModel): - num_retries: int = Field(default=30, ge=-1) - timeout: int = Field(default=10, ge=1) - - -def to_bool(value: str) -> bool: - """Convert a string to a boolean.""" - return value.lower() in ("true", "1", "t", "y", "yes") - - -SETTINGS: HostSettings = HostSettings( - host=str(os.getenv("SVC_HOST", "localhost")), - port=int(os.getenv("SVC_PORT", 8000)), - etcd_host=str(os.getenv("ETCD_HOST", "localhost")), - etcd_port=int(os.getenv("ETCD_PORT", 2379)), - tool_support=to_bool(os.getenv("TOOL_SUPPORT", "False")), - multimodal_support=to_bool(os.getenv("MULTIMODAL_SUPPORT", "False")), - gunicorn_workers=int(os.getenv("NILAI_GUNICORN_WORKERS", 10)), - attestation_host=str(os.getenv("ATTESTATION_HOST", "localhost")), - attestation_port=int(os.getenv("ATTESTATION_PORT", 8081)), -) - -MODEL_SETTINGS: ModelSettings = ModelSettings( - num_retries=int(os.getenv("MODEL_NUM_RETRIES", 30)), - timeout=int(os.getenv("MODEL_RETRY_TIMEOUT", 10)), -) diff --git a/packages/nilai-common/src/nilai_common/config/__init__.py b/packages/nilai-common/src/nilai_common/config/__init__.py new file mode 100644 index 00000000..633d9642 --- /dev/null +++ b/packages/nilai-common/src/nilai_common/config/__init__.py @@ -0,0 +1,14 @@ +"""Configuration module for nilai-common.""" + +from .host import HostSettings, SETTINGS, to_bool +from .model import ModelSettings, ModelCapabilities, MODEL_SETTINGS, MODEL_CAPABILITIES + +__all__ = [ + "HostSettings", + "ModelSettings", + "ModelCapabilities", + "SETTINGS", + "MODEL_SETTINGS", + "MODEL_CAPABILITIES", + "to_bool", +] diff --git a/packages/nilai-common/src/nilai_common/config/host.py b/packages/nilai-common/src/nilai_common/config/host.py new file mode 100644 index 00000000..14e93f7f --- /dev/null +++ b/packages/nilai-common/src/nilai_common/config/host.py @@ -0,0 +1,33 @@ +"""Host and infrastructure configuration settings.""" + +import os +from pydantic import BaseModel, Field + + +def to_bool(value: str) -> bool: + """Convert a string to a boolean.""" + return value.lower() in ("true", "1", "t", "y", "yes") + + +class HostSettings(BaseModel): + """Infrastructure and service host configuration.""" + + host: str = Field(default="localhost", description="Host of the service") + port: int = Field(default=8000, description="Port of the service") + discovery_host: str = Field( + default="localhost", description="Host of the discovery service" + ) + discovery_port: int = Field( + default=6379, description="Port of the discovery service" + ) + gunicorn_workers: int = Field(default=10, description="Number of gunicorn workers") + + +# Global host settings instance +SETTINGS: HostSettings = HostSettings( + host=str(os.getenv("SVC_HOST", "localhost")), + port=int(os.getenv("SVC_PORT", 8000)), + discovery_host=str(os.getenv("DISCOVERY_HOST", "redis")), + discovery_port=int(os.getenv("DISCOVERY_PORT", 6379)), + gunicorn_workers=int(os.getenv("NILAI_GUNICORN_WORKERS", 10)), +) diff --git a/packages/nilai-common/src/nilai_common/config/model.py b/packages/nilai-common/src/nilai_common/config/model.py new file mode 100644 index 00000000..6bfeafab --- /dev/null +++ b/packages/nilai-common/src/nilai_common/config/model.py @@ -0,0 +1,35 @@ +"""Model-specific configuration settings.""" + +import os +from pydantic import BaseModel, Field + +from .host import to_bool + + +class ModelSettings(BaseModel): + """Model retry and timeout configuration.""" + + num_retries: int = Field(default=30, ge=-1, description="Number of retries") + timeout: int = Field(default=10, ge=1, description="Timeout in seconds") + + +class ModelCapabilities(BaseModel): + """Model capability flags.""" + + tool_support: bool = Field(default=False, description="Tool support flag") + multimodal_support: bool = Field( + default=False, description="Multimodal support flag" + ) + + +# Global model settings instance +MODEL_SETTINGS: ModelSettings = ModelSettings( + num_retries=int(os.getenv("MODEL_NUM_RETRIES", 30)), + timeout=int(os.getenv("MODEL_RETRY_TIMEOUT", 10)), +) + +# Global model capabilities instance +MODEL_CAPABILITIES: ModelCapabilities = ModelCapabilities( + tool_support=to_bool(os.getenv("TOOL_SUPPORT", "False")), + multimodal_support=to_bool(os.getenv("MULTIMODAL_SUPPORT", "False")), +) diff --git a/packages/nilai-common/src/nilai_common/discovery.py b/packages/nilai-common/src/nilai_common/discovery.py index a604fecb..345a9a6d 100644 --- a/packages/nilai-common/src/nilai_common/discovery.py +++ b/packages/nilai-common/src/nilai_common/discovery.py @@ -1,15 +1,12 @@ import asyncio import logging -from typing import Dict, Optional - from asyncio import CancelledError from datetime import datetime, timezone -from tenacity import retry, wait_exponential, stop_after_attempt - +from typing import Dict, Optional -from etcd3gw import Lease -from etcd3gw.client import Etcd3Client +import redis.asyncio as redis from nilai_common.api_models import ModelEndpoint, ModelMetadata +from tenacity import retry, stop_after_attempt, wait_exponential # Configure logging logging.basicConfig(level=logging.INFO) @@ -17,18 +14,19 @@ class ModelServiceDiscovery: - def __init__(self, host: str = "localhost", port: int = 2379, lease_ttl: int = 60): + def __init__(self, host: str = "localhost", port: int = 6379, lease_ttl: int = 60): """ - Initialize etcd client for model service discovery. + Initialize Redis client for model service discovery. - :param host: etcd server host - :param port: etcd server port - :param lease_ttl: Lease time for endpoint registration (in seconds) + :param host: Redis server host + :param port: Redis server port + :param lease_ttl: TTL time for endpoint registration (in seconds) """ self.host = host self.port = port self.lease_ttl = lease_ttl - self.initialize() + self._client: Optional[redis.Redis] = None + self._model_key: Optional[str] = None self.is_healthy = True self.last_refresh = None @@ -36,32 +34,49 @@ def __init__(self, host: str = "localhost", port: int = 2379, lease_ttl: int = 6 self.base_delay = 1 self._shutdown = False - def initialize(self): + async def initialize(self): """ - Initialize the etcd client. + Initialize the Redis client. """ - self.client = Etcd3Client(host=self.host, port=self.port) + if self._client is None: + self._client = await redis.Redis( + host=self.host, port=self.port, decode_responses=True + ) + + @property + async def client(self) -> redis.Redis: + """ + Get the Redis client. + """ + if self._client is None: + await self.initialize() + if self._client is None: + # This should never happen + raise ValueError("Redis client must be initialized") + return self._client async def register_model( self, model_endpoint: ModelEndpoint, prefix: str = "/models" - ) -> Lease: + ) -> str: """ - Register a model endpoint in etcd. + Register a model endpoint in Redis. :param model_endpoint: ModelEndpoint to register - :return: Lease ID for the registration + :param prefix: Key prefix for models + :return: The key used for registration """ - # Create a lease for the endpoint - lease = self.client.lease(self.lease_ttl) # Prepare the key and value key = f"{prefix}/{model_endpoint.metadata.id}" value = model_endpoint.model_dump_json() - # Put the key-value pair with the lease - self.client.put(key, value, lease=lease) + # Set the key-value pair with TTL + await (await self.client).setex(key, self.lease_ttl, value) + + # Store the key for keep_alive + self._model_key = key - return lease + return key async def discover_models( self, @@ -74,30 +89,47 @@ async def discover_models( :param name: Optional model name to filter :param feature: Optional feature to filter - :return: List of matching ModelEndpoints + :param prefix: Key prefix for models + :return: Dict of matching ModelEndpoints """ - # Get all model keys - model_range = self.client.get_prefix(f"{prefix}/") - self.client.get_prefix + # Get all model keys using SCAN pattern discovered_models: Dict[str, ModelEndpoint] = {} - for resp, other in model_range: - try: - model_endpoint = ModelEndpoint.model_validate_json(resp.decode("utf-8")) # type: ignore - - # Apply filters if provided - if name and name.lower() not in model_endpoint.metadata.name.lower(): - continue - - if ( - feature - and feature not in model_endpoint.metadata.supported_features - ): - continue - - discovered_models[model_endpoint.metadata.id] = model_endpoint - except Exception as e: - logger.error(f"Error parsing model endpoint: {e}") + pattern = f"{prefix}/*" + + cursor = 0 + while True: + cursor, keys = await (await self.client).scan( + cursor=cursor, match=pattern, count=100 + ) + + for key in keys: + try: + value = await (await self.client).get(key) + if value: + model_endpoint = ModelEndpoint.model_validate_json(value) + + # Apply filters if provided + if ( + name + and name.lower() not in model_endpoint.metadata.name.lower() + ): + continue + + if ( + feature + and feature + not in model_endpoint.metadata.supported_features + ): + continue + + discovered_models[model_endpoint.metadata.id] = model_endpoint + except Exception as e: + logger.error(f"Error parsing model endpoint from key {key}: {e}") + + if cursor == 0: + break + return discovered_models async def get_model( @@ -107,60 +139,95 @@ async def get_model( Get a model endpoint by ID. :param model_id: ID of the model to retrieve + :param prefix: Key prefix for models :return: ModelEndpoint if found, None otherwise """ key = f"{prefix}/{model_id}" - value = self.client.get(key) - value = self.client.get(model_id) if not value else value + value = await (await self.client).get(key) + + # Try without prefix if not found + if not value: + value = await (await self.client).get(model_id) + if value: - return ModelEndpoint.model_validate_json(value[0].decode("utf-8")) # type: ignore + return ModelEndpoint.model_validate_json(value) return None - async def unregister_model(self, model_id: str): + async def unregister_model(self, model_id: str, prefix: str = "/models"): """ Unregister a model from service discovery. :param model_id: ID of the model to unregister + :param prefix: Key prefix for models """ - key = f"/models/{model_id}" - self.client.delete(key) + key = f"{prefix}/{model_id}" + await (await self.client).delete(key) @retry( wait=wait_exponential(multiplier=1, min=4, max=10), stop=stop_after_attempt(3) ) - async def _refresh_lease(self, lease): - lease.refresh() + async def _refresh_ttl(self, key: str, model_json: str): + """Refresh the TTL for a Redis key.""" + await (await self.client).setex(key, self.lease_ttl, model_json) self.last_refresh = datetime.now(timezone.utc) self.is_healthy = True - async def keep_alive(self, lease): - """Keep the model registration lease alive with graceful shutdown.""" + async def keep_alive( + self, key: Optional[str] = None, model_endpoint: Optional[ModelEndpoint] = None + ): + """Keep the model registration alive by refreshing TTL with graceful shutdown.""" + if model_endpoint is None and self._model_key is None: + logger.error("No model endpoint or key provided for keep_alive") + return + + # Use provided key or stored key + active_key = key if key else self._model_key + + if not active_key: + logger.error("No valid key for keep_alive") + return + + # Get the model JSON once + if model_endpoint: + model_json = model_endpoint.model_dump_json() + else: + # Fetch current value if not provided + model_json = await (await self.client).get(active_key) + if not model_json: + logger.error(f"No model found at key {active_key}") + return + try: while not self._shutdown: try: - await self._refresh_lease(lease) + await self._refresh_ttl(active_key, model_json) await asyncio.sleep(self.lease_ttl // 2) except Exception as e: self.is_healthy = False - logger.error(f"Lease keepalive failed: {e}") + logger.error(f"TTL refresh failed: {e}") try: - self.initialize() - lease.client = self.client + await self.initialize() except Exception as init_error: logger.error(f"Reinitialization failed: {init_error}") await asyncio.sleep(self.base_delay) except CancelledError: - logger.info("Lease keepalive task cancelled, shutting down...") + logger.info("Keep-alive task cancelled, shutting down...") self._shutdown = True raise finally: self.is_healthy = False + async def close(self): + """Close the Redis connection.""" + if self._client: + await self._client.aclose() + # Example usage async def main(): # Initialize service discovery service_discovery = ModelServiceDiscovery(lease_ttl=10) + await service_discovery.initialize() # Create a sample model endpoint model_metadata = ModelMetadata( @@ -179,11 +246,12 @@ async def main(): ) # Register the model - lease = await service_discovery.register_model(model_endpoint) + key = await service_discovery.register_model(model_endpoint) + + # Start keeping the registration alive in the background + asyncio.create_task(service_discovery.keep_alive(key, model_endpoint)) + await asyncio.sleep(9) - # Start keeping the lease alive in the background - asyncio.create_task(service_discovery.keep_alive(lease)) - await asyncio.sleep(9) # Keep running for an hour # Discover models (with optional filtering) discovered_models = await service_discovery.discover_models( name="Image Classification", feature="image_classification" @@ -194,9 +262,10 @@ async def main(): logger.info(f"URL: {model.url}") logger.info(f"Supported Features: {model.metadata.supported_features}") - # Optional: Keep the service running - await asyncio.sleep(10) # Keep running for an hour - # Discover models (with optional filtering) + # Keep the service running + await asyncio.sleep(10) + + # Discover models again discovered_models = await service_discovery.discover_models( name="Image Classification", feature="image_classification" ) @@ -208,6 +277,7 @@ async def main(): # Cleanup await service_discovery.unregister_model(model_endpoint.metadata.id) + await service_discovery.close() # This allows running the async main function diff --git a/packages/nilai-common/src/nilai_common/logger.py b/packages/nilai-common/src/nilai_common/logger.py deleted file mode 100644 index e23f71c0..00000000 --- a/packages/nilai-common/src/nilai_common/logger.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import sys -from typing import Optional -from pathlib import Path - - -def setup_logger( - name: str, - level: int = logging.INFO, - log_file: Optional[Path] = None, -) -> logging.Logger: - """Configure common logger for Nilai services.""" - - # Create logger with service name - logger = logging.getLogger(name) - logger.setLevel(level) - - # Create formatter - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - - # Console handler - console_handler = logging.StreamHandler(sys.stdout) - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) - - # File handler if path provided - if log_file: - log_file.parent.mkdir(parents=True, exist_ok=True) - file_handler = logging.FileHandler(str(log_file)) - file_handler.setFormatter(formatter) - logger.addHandler(file_handler) - - return logger diff --git a/prometheus/config/prometheus.yml b/prometheus/config/prometheus.yml deleted file mode 100644 index 5350b22d..00000000 --- a/prometheus/config/prometheus.yml +++ /dev/null @@ -1,13 +0,0 @@ -global: - scrape_interval: 30s - -scrape_configs: - - job_name: "nilai" - scrape_interval: 30s - metrics_path: "/metrics" - static_configs: - - targets: - - "nilai-api:8080" - - "node-exporter:9100" - - diff --git a/prometheus/data/.gitkeep b/prometheus/data/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/pyproject.toml b/pyproject.toml index 324e573a..e89d07ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,20 +11,21 @@ requires-python = ">=3.12" dependencies = [ "nilai-api", "nilai-common", - "nilai-models" + "nilai-models", + "nilai-py", ] [dependency-groups] dev = [ "black>=25.9.0", - "isort>=6.1.0", + "isort>=7.0.0", "pytest-mock>=3.14.0", "pytest>=8.3.3", "ruff>=0.11.7", "uvicorn>=0.32.1", "pytest-asyncio>=1.2.0", "testcontainers>=4.13.0", - "pyright>=1.1.405", + "pyright>=1.1.406", "pre-commit>=4.1.0", "httpx>=0.28.1", ] @@ -37,12 +38,14 @@ build-backend = "setuptools.build_meta" find = { include = ["nilai"] } [tool.uv.workspace] -members = ["nilai-models", "nilai-api", "packages/nilai-common"] +members = ["nilai-models", "nilai-api", "packages/nilai-common", "clients/nilai-py"] [tool.uv.sources] nilai-common = { workspace = true } nilai-api = { workspace = true } nilai-models = { workspace = true } +nilai-py = { workspace = true } + [tool.pyright] exclude = ["**/.venv", "**/.venv/**"] diff --git a/scripts/credit-init.sql b/scripts/credit-init.sql new file mode 100644 index 00000000..57833785 --- /dev/null +++ b/scripts/credit-init.sql @@ -0,0 +1,28 @@ +-- Seed test data for development +-- This migration inserts test users, admin keys, and test credentials + +-- Insert admin key +INSERT INTO admins (key, user_id, is_active) VALUES + ('n i l l i o n', 'admin', true) +ON CONFLICT (key) DO NOTHING; + +-- Insert test users +INSERT INTO users (user_id, balance) VALUES + ('Docs User', 10000.0) +ON CONFLICT (user_id) DO NOTHING; + +-- Insert test credentials for users +-- SecretTestApiKey gets a private credential (API Key to access endpoints) +INSERT INTO credentials (credential_key, user_id, is_public, is_active) VALUES + ('SecretTestApiKey', 'Docs User', false, true) +ON CONFLICT (credential_key) DO NOTHING; + +-- Nillion2025 gets a private credential (API Key to access endpoints) +INSERT INTO credentials (credential_key, user_id, is_public, is_active) VALUES + ('Nillion2025', 'Docs User', false, true) +ON CONFLICT (credential_key) DO NOTHING; + +-- 030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10 gets a public credential (Public Keypair to access endpoints) +INSERT INTO credentials (credential_key, user_id, is_public, is_active) VALUES + ('did:nil:030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10', 'Docs User', true, true) +ON CONFLICT (credential_key) DO NOTHING; diff --git a/conftest.py b/tests/conftest.py similarity index 100% rename from conftest.py rename to tests/conftest.py diff --git a/tests/e2e/config.py b/tests/e2e/config.py index 81797ead..b2ff1968 100644 --- a/tests/e2e/config.py +++ b/tests/e2e/config.py @@ -1,9 +1,9 @@ -from .nuc import get_nuc_token +from .nuc import get_nuc_client from nilai_api.config import CONFIG ENVIRONMENT = CONFIG.environment.environment # Left for API key for backwards compatibility -AUTH_TOKEN = CONFIG.auth.auth_token +AUTH_TOKEN = "SecretTestApiKey" AUTH_STRATEGY = CONFIG.auth.auth_strategy match AUTH_STRATEGY: @@ -17,7 +17,7 @@ def api_key_getter() -> str: if AUTH_STRATEGY == "nuc": - return get_nuc_token().token + return get_nuc_client()._get_invocation_token() elif AUTH_STRATEGY == "api_key": if AUTH_TOKEN is None: raise ValueError("Expected AUTH_TOKEN to be set") diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 00000000..fc57b1c6 --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,239 @@ +from .config import BASE_URL, api_key_getter +from .nuc import ( + get_rate_limited_nuc_token, + get_invalid_rate_limited_nuc_token, + get_document_id_nuc_token, + get_invalid_nildb_nuc_token, +) +import httpx +import pytest +import pytest_asyncio +from openai import OpenAI, AsyncOpenAI + + +# ============================================================================ +# HTTP Client Fixtures (for test_chat_completions_http.py, test_responses_http.py) +# ============================================================================ + + +@pytest.fixture +def http_client(): + """Create an HTTPX client with default headers for HTTP-based tests""" + invocation_token: str = api_key_getter() + print("invocation_token", invocation_token) + return httpx.Client( + base_url=BASE_URL, + headers={ + "accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Bearer {invocation_token}", + }, + verify=False, + timeout=None, + ) + + +# Alias for backward compatibility +client = http_client + + +@pytest.fixture +def rate_limited_http_client(): + """Create an HTTPX client with rate limiting for HTTP-based tests""" + invocation_token = get_rate_limited_nuc_token(rate_limit=1) + return httpx.Client( + base_url=BASE_URL, + headers={ + "accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Bearer {invocation_token}", + }, + timeout=None, + verify=False, + ) + + +# Alias for backward compatibility +rate_limited_client = rate_limited_http_client + + +@pytest.fixture +def invalid_rate_limited_http_client(): + """Create an HTTPX client with invalid rate limiting for HTTP-based tests""" + invocation_token = get_invalid_rate_limited_nuc_token() + return httpx.Client( + base_url=BASE_URL, + headers={ + "accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Bearer {invocation_token}", + }, + timeout=None, + verify=False, + ) + + +# Alias for backward compatibility +invalid_rate_limited_client = invalid_rate_limited_http_client + + +@pytest.fixture +def nillion_2025_client(): + """Create an HTTPX client with default headers""" + return httpx.Client( + base_url=BASE_URL, + headers={ + "accept": "application/json", + "Content-Type": "application/json", + "Authorization": "Bearer Nillion2025", + }, + verify=False, + timeout=None, + ) + + +@pytest.fixture +def document_id_client(): + """Create an HTTPX client with default headers""" + invocation_token = get_document_id_nuc_token() + return httpx.Client( + base_url=BASE_URL, + headers={ + "accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Bearer {invocation_token}", + }, + verify=False, + timeout=None, + ) + + +@pytest.fixture +def invalid_nildb(): + """Create an HTTPX client with default headers""" + invocation_token = get_invalid_nildb_nuc_token() + return httpx.Client( + base_url=BASE_URL, + headers={ + "accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Bearer {invocation_token}", + }, + timeout=None, + verify=False, + ) + + +# ============================================================================ +# OpenAI SDK Client Fixtures (for test_chat_completions.py, test_responses.py) +# ============================================================================ + + +def _create_openai_client(api_key: str) -> OpenAI: + """Helper function to create an OpenAI client with SSL verification disabled""" + transport = httpx.HTTPTransport(verify=False) + return OpenAI( + base_url=BASE_URL, + api_key=api_key, + http_client=httpx.Client(transport=transport), + ) + + +def _create_async_openai_client(api_key: str) -> AsyncOpenAI: + """Helper function to create an async OpenAI client with SSL verification disabled""" + transport = httpx.AsyncHTTPTransport(verify=False) + return AsyncOpenAI( + base_url=BASE_URL, + api_key=api_key, + http_client=httpx.AsyncClient(transport=transport), + ) + + +@pytest.fixture +def openai_client(): + """Create an OpenAI SDK client configured to use the Nilai API""" + invocation_token: str = api_key_getter() + return _create_openai_client(invocation_token) + + +@pytest_asyncio.fixture +async def async_openai_client(): + """Create an async OpenAI SDK client configured to use the Nilai API""" + invocation_token: str = api_key_getter() + transport = httpx.AsyncHTTPTransport(verify=False) + httpx_client = httpx.AsyncClient(transport=transport) + client = AsyncOpenAI( + base_url=BASE_URL, api_key=invocation_token, http_client=httpx_client + ) + yield client + await httpx_client.aclose() + + +@pytest.fixture +def rate_limited_openai_client(): + """Create an OpenAI SDK client with rate limiting""" + invocation_token = get_rate_limited_nuc_token(rate_limit=1) + return _create_openai_client(invocation_token) + + +@pytest.fixture +def invalid_rate_limited_openai_client(): + """Create an OpenAI SDK client with invalid rate limiting""" + invocation_token = get_invalid_rate_limited_nuc_token() + return _create_openai_client(invocation_token) + + +@pytest.fixture +def document_id_openai_client(): + """Create an OpenAI SDK client with document ID token""" + invocation_token = get_document_id_nuc_token() + return _create_openai_client(invocation_token) + + +@pytest.fixture +def invalid_nildb_openai_client(): + """Create an OpenAI SDK client with document ID token""" + invocation_token = get_invalid_nildb_nuc_token() + return _create_openai_client(invocation_token) + + +@pytest.fixture +def high_web_search_rate_limit(monkeypatch): + """Set high rate limits for web search for RPS tests""" + monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_MINUTE", "9999") + monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_HOUR", "9999") + monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_DAY", "9999") + monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT", "9999") + monkeypatch.setenv("USER_RATE_LIMIT_MINUTE", "9999") + monkeypatch.setenv("USER_RATE_LIMIT_HOUR", "9999") + monkeypatch.setenv("USER_RATE_LIMIT_DAY", "9999") + monkeypatch.setenv("USER_RATE_LIMIT", "9999") + monkeypatch.setenv( + "MODEL_CONCURRENT_RATE_LIMIT", + ( + '{"meta-llama/Llama-3.2-1B-Instruct": 500, ' + '"meta-llama/Llama-3.2-3B-Instruct": 500, ' + '"meta-llama/Llama-3.1-8B-Instruct": 300, ' + '"cognitivecomputations/Dolphin3.0-Llama3.1-8B": 300, ' + '"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": 50, ' + '"hugging-quants/Meta-Llama-3.1-70B-Instruct-AWQ-INT4": 50, ' + '"openai/gpt-oss-20b": 500, ' + '"google/gemma-3-27b-it": 500, ' + '"default": 500}' + ), + ) + + +# ============================================================================ +# Convenience Aliases for OpenAI SDK Tests +# These allow test files to use 'client' instead of 'openai_client' +# Note: These will be shadowed by local fixtures in test_chat_completions.py +# and test_responses.py if those files redefine them +# ============================================================================ + +# Uncomment these if you want to use the conftest fixtures without shadowing: +# client = openai_client +# async_client = async_openai_client +# rate_limited_client = rate_limited_openai_client +# invalid_rate_limited_client = invalid_rate_limited_openai_client +nildb_client = document_id_openai_client diff --git a/tests/e2e/nuc.py b/tests/e2e/nuc.py index 2c2366a9..f63248c8 100644 --- a/tests/e2e/nuc.py +++ b/tests/e2e/nuc.py @@ -1,21 +1,21 @@ -from datetime import datetime, timedelta, timezone from nilai_api.auth.nuc_helpers import ( - get_wallet_and_private_key, - pay_for_subscription, - get_root_token, - get_nilai_public_key, - get_invocation_token as nuc_helpers_get_invocation_token, - validate_token, - InvocationToken, - RootToken, - NilAuthPublicKey, NilAuthPrivateKey, - get_delegation_token, - DelegationToken, ) -from nuc.nilauth import NilauthClient, BlindModule -from nuc.token import Did -from nuc.validate import ValidationParameters, InvocationRequirement + +from nuc.builder import NucTokenBuilder +from nuc.token import Did, InvocationBody, Command + +from nilai_py import ( + Client, + DelegationTokenServer, + DelegationServerConfig, + AuthType, + DelegationTokenRequest, + DelegationTokenResponse, + PromptDocumentInfo, +) +from openai import DefaultHttpxClient + # These correspond to the key used to test with nilAuth. Otherwise the OWNER DID would not match the issuer DOCUMENT_ID = "bb93f3a4-ba4c-4e20-8f2e-c0650c75a372" @@ -23,16 +23,16 @@ "did:nil:030923f2e7120c50e42905b857ddd2947f6ecced6bb02aab64e63b28e9e2e06d10" ) +PRIVATE_KEY = "97f49889fceed88a9cdddb16a161d13f6a12307c2b39163f3c3c397c3c2d2434" # Example private key for testing devnet + -def get_nuc_token( +def get_nuc_client( usage_limit: int | None = None, - expires_at: datetime | None = None, - blind_module: BlindModule = BlindModule.NILAI, + expires_in: int | None = None, document_id: str | None = None, document_owner_did: str | None = None, - create_delegation: bool = False, create_invalid_delegation: bool = False, -) -> InvocationToken: +) -> Client: """ Unified function to get NUC tokens with various configurations. @@ -46,145 +46,105 @@ def get_nuc_token( Returns: InvocationToken: The generated token """ - # Constants - PRIVATE_KEY = "l/SYifzu2Iqc3dsWoWHRP2oSMHwrORY/PDw5fDwtJDQ=" # Example private key for testing devnet - NILAI_ENDPOINT = "localhost:8080" - NILAUTH_ENDPOINT = "localhost:30921" - NILCHAIN_GRPC = "localhost:26649" - - # Setup server private key and client - server_wallet, server_keypair, server_private_key = get_wallet_and_private_key( + # We use a key that is not registered to nilauth-credit for the invalid delegation token + + private_key = ( PRIVATE_KEY + if not create_invalid_delegation + else NilAuthPrivateKey().serialize() ) - print("Public key: ", server_private_key.pubkey) - nilauth_client = NilauthClient(f"http://{NILAUTH_ENDPOINT}") - - if not server_private_key.pubkey: - raise Exception("Failed to get public key") - - # Pay for subscription - pay_for_subscription( - nilauth_client, - server_wallet, - server_keypair, - server_private_key.pubkey, - f"http://{NILCHAIN_GRPC}", - blind_module=blind_module, + config = DelegationServerConfig( + expiration_time=expires_in if expires_in else 10 * 60 * 60, # 10 hours + token_max_uses=usage_limit if usage_limit else 10, + prompt_document=PromptDocumentInfo( + doc_id=document_id if document_id else DOCUMENT_ID, + owner_did=document_owner_did if document_owner_did else DOCUMENT_OWNER_DID, + ) + if document_id or document_owner_did + else None, ) - # Create root token - root_token: RootToken = get_root_token( - nilauth_client, - server_private_key, - blind_module=blind_module, + root_server = DelegationTokenServer( + private_key=private_key, + config=config, ) - # Get Nilai public key - nilai_public_key: NilAuthPublicKey = get_nilai_public_key( - f"http://{NILAI_ENDPOINT}" + # >>> Client initializes a client + # The client is responsible for making requests to the Nilai API. + # We do not provide an API key but we set the auth type to DELEGATION_TOKEN + http_client = DefaultHttpxClient(verify=False) + client = Client( + base_url="https://localhost/nuc/v1", + auth_type=AuthType.DELEGATION_TOKEN, + http_client=http_client, + api_key=private_key, ) - # Handle delegation token creation if requested - if create_delegation or create_invalid_delegation: - # Create user private key and public key - user_private_key = NilAuthPrivateKey() - user_public_key = user_private_key.pubkey - - if user_public_key is None: - raise Exception("Failed to get user public key") - - # Set default values for delegation - delegation_usage_limit = usage_limit if usage_limit is not None else 3 - delegation_expires_at = ( - expires_at - if expires_at is not None - else datetime.now(timezone.utc) + timedelta(minutes=5) - ) + delegation_request: DelegationTokenRequest = client.get_delegation_request() - # Create delegation token - delegation_token: DelegationToken = get_delegation_token( - root_token, - server_private_key, - user_public_key, - usage_limit=delegation_usage_limit, - expires_at=delegation_expires_at, - document_id=document_id, - document_owner_did=document_owner_did, - ) + # <<< Server creates a delegation token + delegation_token: DelegationTokenResponse = root_server.create_delegation_token( + delegation_request + ) + # >>> Client sets internally the delegation token + client.update_delegation(delegation_token) - # Create invalid delegation chain if requested (for testing) - if create_invalid_delegation: - delegation_token = get_delegation_token( - delegation_token, - user_private_key, - user_public_key, - usage_limit=5, - expires_at=datetime.now(timezone.utc) + timedelta(minutes=5), - document_id=document_id, - document_owner_did=document_owner_did, - ) - - # Create invocation token from delegation - invocation_token: InvocationToken = nuc_helpers_get_invocation_token( - delegation_token, - nilai_public_key, - user_private_key, - ) - else: - # Create invocation token directly from root token - invocation_token: InvocationToken = nuc_helpers_get_invocation_token( - root_token, - nilai_public_key, - server_private_key, - ) + return client - # Validate the token - default_validation_parameters = ValidationParameters.default() - default_validation_parameters.token_requirements = InvocationRequirement( - audience=Did(nilai_public_key.serialize()) - ) - validate_token( - f"http://{NILAUTH_ENDPOINT}", - invocation_token.token, - default_validation_parameters, +def get_rate_limited_nuc_client(rate_limit: int = 3) -> Client: + return get_nuc_client( + usage_limit=rate_limit, + expires_in=5 * 60, # 5 minutes ) - return invocation_token - -def get_rate_limited_nuc_token(rate_limit: int = 3) -> InvocationToken: +def get_rate_limited_nuc_token(rate_limit: int = 3) -> str: """Convenience function for getting rate-limited tokens.""" - return get_nuc_token( - usage_limit=rate_limit, - expires_at=datetime.now(timezone.utc) + timedelta(minutes=5), - create_delegation=True, + return get_rate_limited_nuc_client(rate_limit)._get_invocation_token() + + +def get_invalid_rate_limited_nuc_client() -> Client: + return get_nuc_client( + usage_limit=3, + expires_in=5 * 60, # 5 minutes + create_invalid_delegation=True, ) -def get_document_id_nuc_token() -> InvocationToken: - """Convenience function for getting NILDB NUC tokens.""" - print("DOCUMENT_ID", DOCUMENT_ID) - return get_nuc_token( - create_delegation=True, +def get_invalid_rate_limited_nuc_token() -> str: + return get_invalid_rate_limited_nuc_client()._get_invocation_token() + + +def get_document_id_nuc_client() -> Client: + return get_nuc_client( document_id=DOCUMENT_ID, document_owner_did=DOCUMENT_OWNER_DID, ) -def get_invalid_rate_limited_nuc_token() -> InvocationToken: - """Convenience function for getting invalid rate-limited tokens (for testing).""" - return get_nuc_token( - usage_limit=3, - expires_at=datetime.now(timezone.utc) + timedelta(minutes=5), - create_delegation=True, - create_invalid_delegation=True, - ) +def get_document_id_nuc_token() -> str: + """Convenience function for getting NILDB NUC tokens.""" + return get_document_id_nuc_client()._get_invocation_token() -def get_nildb_nuc_token() -> InvocationToken: +def get_invalid_nildb_nuc_token() -> str: """Convenience function for getting NILDB NUC tokens.""" - return get_nuc_token( - blind_module=BlindModule.NILDB, + private_key = NilAuthPrivateKey(bytes.fromhex(PRIVATE_KEY)) + http_client = DefaultHttpxClient(verify=False) + client = Client( + base_url="https://localhost/nuc/v1", + auth_type=AuthType.API_KEY, + http_client=http_client, + api_key=PRIVATE_KEY, + ) + + invocation_token: str = ( + NucTokenBuilder.extending(client.root_token) + .body(InvocationBody(args={})) + .audience(Did(client.nilai_public_key.serialize())) + .command(Command(["nil", "db", "generate"])) + .build(private_key) ) + return invocation_token diff --git a/tests/e2e/test_chat_completions.py b/tests/e2e/test_chat_completions.py index 72b491f9..dd362885 100644 --- a/tests/e2e/test_chat_completions.py +++ b/tests/e2e/test_chat_completions.py @@ -11,105 +11,41 @@ import json import os import re -import httpx import pytest import pytest_asyncio -from openai import OpenAI -from openai import AsyncOpenAI from openai.types.chat import ChatCompletion -from .config import BASE_URL, test_models, AUTH_STRATEGY, api_key_getter -from .nuc import ( - get_rate_limited_nuc_token, - get_invalid_rate_limited_nuc_token, - get_nildb_nuc_token, -) - +from .config import BASE_URL, ENVIRONMENT, test_models, AUTH_STRATEGY, api_key_getter -def _create_openai_client(api_key: str) -> OpenAI: - """Helper function to create an OpenAI client with SSL verification disabled""" - transport = httpx.HTTPTransport(verify=False) - return OpenAI( - base_url=BASE_URL, - api_key=api_key, - http_client=httpx.Client(transport=transport), - ) - -def _create_async_openai_client(api_key: str) -> AsyncOpenAI: - transport = httpx.AsyncHTTPTransport(verify=False) - return AsyncOpenAI( - base_url=BASE_URL, - api_key=api_key, - http_client=httpx.AsyncClient(transport=transport), - ) +# ============================================================================ +# Fixture Aliases for OpenAI SDK Tests +# These create local aliases that reference the centralized fixtures in conftest.py +# This allows tests to use 'client' instead of 'openai_client', maintaining backward compatibility +# ============================================================================ @pytest.fixture -def client(): - """Create an OpenAI client configured to use the Nilai API""" - invocation_token: str = api_key_getter() - - return _create_openai_client(invocation_token) +def client(openai_client): + """Alias for openai_client fixture from conftest.py""" + return openai_client @pytest_asyncio.fixture -async def async_client(): - invocation_token: str = api_key_getter() - transport = httpx.AsyncHTTPTransport(verify=False) - httpx_client = httpx.AsyncClient(transport=transport) - client = AsyncOpenAI( - base_url=BASE_URL, api_key=invocation_token, http_client=httpx_client - ) - yield client - await httpx_client.aclose() - - -@pytest.fixture -def rate_limited_client(): - """Create an OpenAI client configured to use the Nilai API with rate limiting""" - invocation_token = get_rate_limited_nuc_token(rate_limit=1) - return _create_openai_client(invocation_token.token) +async def async_client(async_openai_client): + """Alias for async_openai_client fixture from conftest.py""" + return async_openai_client @pytest.fixture -def invalid_rate_limited_client(): - """Create an OpenAI client configured to use the Nilai API with rate limiting""" - invocation_token = get_invalid_rate_limited_nuc_token() - return _create_openai_client(invocation_token.token) +def rate_limited_client(rate_limited_openai_client): + """Alias for rate_limited_openai_client fixture from conftest.py""" + return rate_limited_openai_client @pytest.fixture -def nildb_client(): - """Create an OpenAI client configured to use the Nilai API with rate limiting""" - invocation_token = get_nildb_nuc_token() - return _create_openai_client(invocation_token.token) - - -@pytest.fixture -def high_web_search_rate_limit(monkeypatch): - """Set high rate limits for web search for RPS tests""" - monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_MINUTE", "9999") - monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_HOUR", "9999") - monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_DAY", "9999") - monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT", "9999") - monkeypatch.setenv("USER_RATE_LIMIT_MINUTE", "9999") - monkeypatch.setenv("USER_RATE_LIMIT_HOUR", "9999") - monkeypatch.setenv("USER_RATE_LIMIT_DAY", "9999") - monkeypatch.setenv("USER_RATE_LIMIT", "9999") - monkeypatch.setenv( - "MODEL_CONCURRENT_RATE_LIMIT", - ( - '{"meta-llama/Llama-3.2-1B-Instruct": 500, ' - '"meta-llama/Llama-3.2-3B-Instruct": 500, ' - '"meta-llama/Llama-3.1-8B-Instruct": 300, ' - '"cognitivecomputations/Dolphin3.0-Llama3.1-8B": 300, ' - '"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": 50, ' - '"hugging-quants/Meta-Llama-3.1-70B-Instruct-AWQ-INT4": 50, ' - '"openai/gpt-oss-20b": 500, ' - '"google/gemma-3-27b-it": 500, ' - '"default": 500}' - ), - ) +def invalid_rate_limited_client(invalid_rate_limited_openai_client): + """Alias for invalid_rate_limited_openai_client fixture from conftest.py""" + return invalid_rate_limited_openai_client @pytest.mark.parametrize( @@ -201,72 +137,6 @@ def test_rate_limiting_nucs(rate_limited_client, model): assert rate_limited, "No NUC rate limiting detected, when expected" -@pytest.mark.parametrize( - "model", - test_models, -) -@pytest.mark.skipif( - AUTH_STRATEGY != "nuc", reason="NUC rate limiting not used with API key" -) -def test_invalid_rate_limiting_nucs(invalid_rate_limited_client, model): - """Test rate limiting by sending multiple rapid requests""" - import openai - - # Send multiple rapid requests - forbidden = False - for _ in range(4): # Adjust number based on expected rate limits - try: - _ = invalid_rate_limited_client.chat.completions.create( - model=model, - messages=[ - { - "role": "system", - "content": "You are a helpful assistant that provides accurate and concise information.", - }, - {"role": "user", "content": "What is the capital of France?"}, - ], - temperature=0.2, - max_tokens=100, - ) - except openai.AuthenticationError: - forbidden = True - - assert forbidden, "No NUC rate limiting detected, when expected" - - -@pytest.mark.parametrize( - "model", - test_models, -) -@pytest.mark.skipif( - AUTH_STRATEGY != "nuc", reason="NUC rate limiting not used with API key" -) -def test_invalid_nildb_command_nucs(nildb_client, model): - """Test rate limiting by sending multiple rapid requests""" - import openai - - # Send multiple rapid requests - forbidden = False - for _ in range(4): # Adjust number based on expected rate limits - try: - _ = nildb_client.chat.completions.create( - model=model, - messages=[ - { - "role": "system", - "content": "You are a helpful assistant that provides accurate and concise information.", - }, - {"role": "user", "content": "What is the capital of France?"}, - ], - temperature=0.2, - max_tokens=100, - ) - except openai.AuthenticationError: - forbidden = True - - assert forbidden, "No NILDB command detected, when expected" - - @pytest.mark.parametrize( "model", test_models, @@ -553,12 +423,7 @@ def test_usage_endpoint(client): assert isinstance(usage_data, dict), "Usage data should be a dictionary" # Check for expected keys - expected_keys = [ - "total_tokens", - "completion_tokens", - "prompt_tokens", - "queries", - ] + expected_keys = ["total_tokens", "completion_tokens", "prompt_tokens"] for key in expected_keys: assert key in usage_data, f"Expected key {key} not found in usage data" @@ -568,6 +433,10 @@ def test_usage_endpoint(client): pytest.fail(f"Error testing usage endpoint: {str(e)}") +@pytest.mark.skipif( + ENVIRONMENT != "mainnet", + reason="Attestation endpoint not available in non-mainnet environment", +) def test_attestation_endpoint(client): """Test retrieving attestation report""" try: diff --git a/tests/e2e/test_chat_completions_http.py b/tests/e2e/test_chat_completions_http.py index ef7fd9e0..6791f830 100644 --- a/tests/e2e/test_chat_completions_http.py +++ b/tests/e2e/test_chat_completions_http.py @@ -12,112 +12,11 @@ import os import re -from .config import BASE_URL, test_models, AUTH_STRATEGY, api_key_getter -from .nuc import ( - get_rate_limited_nuc_token, - get_invalid_rate_limited_nuc_token, - get_nildb_nuc_token, - get_document_id_nuc_token, -) +from .config import BASE_URL, ENVIRONMENT, test_models, AUTH_STRATEGY import httpx import pytest -@pytest.fixture -def client(): - """Create an HTTPX client with default headers""" - invocation_token: str = api_key_getter() - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token}", - }, - verify=False, - timeout=None, - ) - - -@pytest.fixture -def rate_limited_client(): - """Create an HTTPX client with default headers""" - invocation_token = get_rate_limited_nuc_token(rate_limit=1) - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token.token}", - }, - timeout=None, - verify=False, - ) - - -@pytest.fixture -def invalid_rate_limited_client(): - """Create an HTTPX client with default headers""" - invocation_token = get_invalid_rate_limited_nuc_token() - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token.token}", - }, - timeout=None, - verify=False, - ) - - -@pytest.fixture -def nildb_client(): - """Create an HTTPX client with default headers""" - invocation_token = get_nildb_nuc_token() - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token.token}", - }, - timeout=None, - verify=False, - ) - - -@pytest.fixture -def nillion_2025_client(): - """Create an HTTPX client with default headers""" - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": "Bearer Nillion2025", - }, - verify=False, - timeout=None, - ) - - -@pytest.fixture -def document_id_client(): - """Create an HTTPX client with default headers""" - invocation_token = get_document_id_nuc_token() - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token.token}", - }, - verify=False, - timeout=None, - ) - - def test_health_endpoint(client): """Test the health endpoint""" response = client.get("health") @@ -155,13 +54,16 @@ def test_usage_endpoint(client): "total_tokens", "completion_tokens", "prompt_tokens", - "queries", ] for key in expected_keys: assert key in usage_data, f"Expected key {key} not found in usage data" +@pytest.mark.skipif( + ENVIRONMENT != "mainnet", + reason="Attestation endpoint not available in non-mainnet environment", +) def test_attestation_endpoint(client): """Test the attestation endpoint""" response = client.get("/attestation/report") @@ -584,48 +486,6 @@ def test_rate_limiting_nucs(rate_limited_client): ) -@pytest.mark.skipif( - AUTH_STRATEGY != "nuc", reason="NUC rate limiting not used with API key" -) -def test_invalid_rate_limiting_nucs(invalid_rate_limited_client): - """Test rate limiting by sending multiple rapid requests""" - # Payload for repeated requests - payload = { - "model": test_models[0], - "messages": [{"role": "user", "content": "What is your name?"}], - } - - # Send multiple rapid requests - responses = [] - for _ in range(4): # Adjust number based on expected rate limits - response = invalid_rate_limited_client.post("/chat/completions", json=payload) - responses.append(response) - - # Check for potential rate limit responses - rate_limit_statuses = [401] - rate_limited_responses = [ - r for r in responses if r.status_code in rate_limit_statuses - ] - - assert len(rate_limited_responses) > 0, ( - "No NUC rate limiting detected, when expected" - ) - - -@pytest.mark.skipif( - AUTH_STRATEGY != "nuc", reason="NUC rate limiting not used with API key" -) -def test_invalid_nildb_command_nucs(nildb_client): - """Test rate limiting by sending multiple rapid requests""" - # Payload for repeated requests - payload = { - "model": test_models[0], - "messages": [{"role": "user", "content": "What is your name?"}], - } - response = nildb_client.post("/chat/completions", json=payload) - assert response.status_code == 401, "Invalid NILDB command should return 401" - - def test_large_payload_handling(client): """Test handling of large input payloads""" # Create a very large system message @@ -867,6 +727,9 @@ def test_nildb_delegation(client: httpx.Client): ) def test_nildb_prompt_document(document_id_client: httpx.Client, model): """Tests getting a prompt document from nilDB and executing a chat completion with it""" + pytest.skip( + "Skipping test_nildb_prompt_document because it requires a newer version of secretvaults-py" + ) payload = { "model": model, "messages": [ diff --git a/tests/e2e/test_responses.py b/tests/e2e/test_responses.py index f5f931c5..79679d7c 100644 --- a/tests/e2e/test_responses.py +++ b/tests/e2e/test_responses.py @@ -1,80 +1,52 @@ import json import os -import httpx import pytest import pytest_asyncio -from openai import OpenAI -from openai import AsyncOpenAI - -from .config import BASE_URL, test_models, AUTH_STRATEGY, api_key_getter -from .nuc import ( - get_rate_limited_nuc_token, - get_invalid_rate_limited_nuc_token, - get_nildb_nuc_token, -) - -def _create_openai_client(api_key: str) -> OpenAI: - """Helper function to create an OpenAI client with SSL verification disabled""" - transport = httpx.HTTPTransport(verify=False) - return OpenAI( - base_url=BASE_URL, - api_key=api_key, - http_client=httpx.Client(transport=transport), - ) +from .config import BASE_URL, test_models, AUTH_STRATEGY, api_key_getter, ENVIRONMENT -def _create_async_openai_client(api_key: str) -> AsyncOpenAI: - transport = httpx.AsyncHTTPTransport(verify=False) - return AsyncOpenAI( - base_url=BASE_URL, - api_key=api_key, - http_client=httpx.AsyncClient(transport=transport), - ) +# ============================================================================ +# Fixture Aliases for OpenAI SDK Tests +# These create local aliases that reference the centralized fixtures in conftest.py +# This allows tests to use 'client' instead of 'openai_client', maintaining backward compatibility +# ============================================================================ @pytest.fixture -def client(): - invocation_token: str = api_key_getter() - return _create_openai_client(invocation_token) +def client(openai_client): + """Alias for openai_client fixture from conftest.py""" + return openai_client @pytest_asyncio.fixture -async def async_client(): - invocation_token: str = api_key_getter() - transport = httpx.AsyncHTTPTransport(verify=False) - httpx_client = httpx.AsyncClient(transport=transport) - client = AsyncOpenAI( - base_url=BASE_URL, api_key=invocation_token, http_client=httpx_client - ) - yield client - await httpx_client.aclose() +async def async_client(async_openai_client): + """Alias for async_openai_client fixture from conftest.py""" + return async_openai_client @pytest.fixture -def rate_limited_client(): - invocation_token = get_rate_limited_nuc_token(rate_limit=1) - return _create_openai_client(invocation_token.token) +def rate_limited_client(rate_limited_openai_client): + """Alias for rate_limited_openai_client fixture from conftest.py""" + return rate_limited_openai_client @pytest.fixture -def invalid_rate_limited_client(): - invocation_token = get_invalid_rate_limited_nuc_token() - return _create_openai_client(invocation_token.token) +def invalid_rate_limited_client(invalid_rate_limited_openai_client): + """Alias for invalid_rate_limited_openai_client fixture from conftest.py""" + return invalid_rate_limited_openai_client @pytest.fixture -def nildb_client(): - invocation_token = get_nildb_nuc_token() - return _create_openai_client(invocation_token.token) +def nildb_client(document_id_openai_client): + """Alias for document_id_openai_client fixture from conftest.py""" + return document_id_openai_client @pytest.fixture -def high_web_search_rate_limit(monkeypatch): - monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_MINUTE", "9999") - monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_HOUR", "9999") - monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT_DAY", "9999") - monkeypatch.setenv("WEB_SEARCH_RATE_LIMIT", "9999") +def invalid_nildb(invalid_nildb_openai_client): + """Alias for invalid_nildb_openai_client fixture from conftest.py""" + return invalid_nildb_openai_client @pytest.mark.parametrize("model", test_models) @@ -195,14 +167,14 @@ def test_invalid_rate_limiting_nucs(invalid_rate_limited_client, model): @pytest.mark.skipif( AUTH_STRATEGY != "nuc", reason="NUC rate limiting not used with API key" ) -def test_invalid_nildb_command_nucs(nildb_client, model): +def test_invalid_nildb_command_nucs(invalid_nildb, model): """Test invalid NILDB command handling""" import openai forbidden = False for _ in range(4): try: - nildb_client.responses.create( + invalid_nildb.responses.create( model=model, input="What is the capital of France?", instructions="You are a helpful assistant that provides accurate and concise information.", @@ -481,7 +453,6 @@ def test_usage_endpoint(client): "total_tokens", "completion_tokens", "prompt_tokens", - "queries", ] for key in expected_keys: assert key in usage_data, f"Expected key {key} not found in usage data" @@ -492,6 +463,10 @@ def test_usage_endpoint(client): pytest.fail(f"Error testing usage endpoint: {str(e)}") +@pytest.mark.skipif( + ENVIRONMENT != "mainnet", + reason="Attestation endpoint not available in non-mainnet environment", +) def test_attestation_endpoint(client): """Test retrieving attestation report""" try: diff --git a/tests/e2e/test_responses_http.py b/tests/e2e/test_responses_http.py index a92c8ddf..17d632a3 100644 --- a/tests/e2e/test_responses_http.py +++ b/tests/e2e/test_responses_http.py @@ -4,102 +4,7 @@ import httpx import pytest -from .config import BASE_URL, test_models, AUTH_STRATEGY, api_key_getter -from .nuc import ( - get_rate_limited_nuc_token, - get_invalid_rate_limited_nuc_token, - get_nildb_nuc_token, - get_document_id_nuc_token, -) - - -@pytest.fixture -def client(): - invocation_token: str = api_key_getter() - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token}", - }, - verify=False, - timeout=None, - ) - - -@pytest.fixture -def rate_limited_client(): - invocation_token = get_rate_limited_nuc_token(rate_limit=1) - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token.token}", - }, - timeout=None, - verify=False, - ) - - -@pytest.fixture -def invalid_rate_limited_client(): - invocation_token = get_invalid_rate_limited_nuc_token() - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token.token}", - }, - timeout=None, - verify=False, - ) - - -@pytest.fixture -def nildb_client(): - invocation_token = get_nildb_nuc_token() - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token.token}", - }, - timeout=None, - verify=False, - ) - - -@pytest.fixture -def nillion_2025_client(): - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": "Bearer Nillion2025", - }, - verify=False, - timeout=None, - ) - - -@pytest.fixture -def document_id_client(): - invocation_token = get_document_id_nuc_token() - return httpx.Client( - base_url=BASE_URL, - headers={ - "accept": "application/json", - "Content-Type": "application/json", - "Authorization": f"Bearer {invocation_token.token}", - }, - verify=False, - timeout=None, - ) +from .config import BASE_URL, test_models, AUTH_STRATEGY, api_key_getter, ENVIRONMENT @pytest.mark.parametrize("model", test_models) @@ -530,12 +435,12 @@ def test_invalid_rate_limiting_nucs(invalid_rate_limited_client): @pytest.mark.skipif( AUTH_STRATEGY != "nuc", reason="NUC rate limiting not used with API key" ) -def test_invalid_nildb_command_nucs(nildb_client): +def test_invalid_nildb_command_nucs(invalid_nildb): payload = { "model": test_models[0], "input": "What is your name?", } - response = nildb_client.post("/responses", json=payload) + response = invalid_nildb.post("/responses", json=payload) assert response.status_code == 401, "Invalid NILDB command should return 401" @@ -722,7 +627,6 @@ def test_usage_endpoint(client): "total_tokens", "completion_tokens", "prompt_tokens", - "queries", ] for key in expected_keys: assert key in usage_data, f"Expected key {key} not found in usage data" @@ -733,6 +637,10 @@ def test_usage_endpoint(client): pytest.fail(f"Error testing usage endpoint: {str(e)}") +@pytest.mark.skipif( + ENVIRONMENT != "mainnet", + reason="Attestation endpoint not available in non-mainnet environment", +) def test_attestation_endpoint(client): try: import requests diff --git a/tests/integration/nilai_api/test_users_db_integration.py b/tests/integration/nilai_api/test_users_db_integration.py index 82d8d022..a9f5663a 100644 --- a/tests/integration/nilai_api/test_users_db_integration.py +++ b/tests/integration/nilai_api/test_users_db_integration.py @@ -4,6 +4,7 @@ These tests use a real PostgreSQL database via testcontainers. """ +import uuid import pytest import json @@ -17,37 +18,19 @@ class TestUserManagerIntegration: async def test_simple_user_creation(self, clean_database): """Test creating a simple user and retrieving it.""" # Insert user with minimal data - user = await UserManager.insert_user(name="Simple Test User") + user = await UserManager.insert_user(user_id="Simple Test User") # Verify user creation - assert user.name == "Simple Test User" - assert user.userid is not None - assert user.apikey is not None - assert user.userid != user.apikey # Should be different UUIDs + assert user.user_id == "Simple Test User" + assert user.rate_limits is None, ( + f"Rate limits are not set for user {user.user_id}" + ) # Retrieve user by ID - found_user = await UserManager.check_user(user.userid) + found_user = await UserManager.check_user(user.user_id) assert found_user is not None - assert found_user.userid == user.userid - assert found_user.name == "Simple Test User" - assert found_user.apikey == user.apikey - - @pytest.mark.asyncio - async def test_api_key_validation(self, clean_database): - """Test API key validation functionality.""" - # Create user - user = await UserManager.insert_user("API Test User") - - # Validate correct API key - api_user = await UserManager.check_api_key(user.apikey) - assert api_user is not None - assert api_user.apikey == user.apikey - assert api_user.userid == user.userid - assert api_user.name == "API Test User" - - # Test invalid API key - invalid_user = await UserManager.check_api_key("invalid-api-key") - assert invalid_user is None + assert found_user.user_id == user.user_id + assert found_user.rate_limits == user.rate_limits @pytest.mark.asyncio async def test_rate_limits_json_crud_basic(self, clean_database): @@ -66,14 +49,14 @@ async def test_rate_limits_json_crud_basic(self, clean_database): # CREATE: Insert user with rate limits user = await UserManager.insert_user( - name="Rate Limits Test User", rate_limits=rate_limits + user_id="Rate Limits Test User", rate_limits=rate_limits ) # Verify rate limits are stored as JSON assert user.rate_limits == rate_limits.model_dump() # READ: Retrieve user and verify rate limits JSON - retrieved_user = await UserManager.check_user(user.userid) + retrieved_user = await UserManager.check_user(user.user_id) assert retrieved_user is not None assert retrieved_user.rate_limits == rate_limits.model_dump() @@ -98,11 +81,11 @@ async def test_rate_limits_json_update(self, clean_database): ) user = await UserManager.insert_user( - name="Update Rate Limits User", rate_limits=initial_rate_limits + user_id="Update Rate Limits User", rate_limits=initial_rate_limits ) # Verify initial rate limits - retrieved_user = await UserManager.check_user(user.userid) + retrieved_user = await UserManager.check_user(user.user_id) assert retrieved_user is not None assert retrieved_user.rate_limits == initial_rate_limits.model_dump() @@ -125,19 +108,19 @@ async def test_rate_limits_json_update(self, clean_database): stmt = sa.text(""" UPDATE users SET rate_limits = :rate_limits_json - WHERE userid = :userid + WHERE user_id = :user_id """) await session.execute( stmt, { "rate_limits_json": updated_rate_limits.model_dump_json(), - "userid": user.userid, + "user_id": user.user_id, }, ) await session.commit() # READ: Verify the update worked - updated_user = await UserManager.check_user(user.userid) + updated_user = await UserManager.check_user(user.user_id) assert updated_user is not None assert updated_user.rate_limits == updated_rate_limits.model_dump() @@ -162,11 +145,11 @@ async def test_rate_limits_json_partial_update(self, clean_database): ) user = await UserManager.insert_user( - name="Partial Rate Limits User", rate_limits=partial_rate_limits + user_id="Partial Rate Limits User", rate_limits=partial_rate_limits ) # Verify partial data is stored correctly - retrieved_user = await UserManager.check_user(user.userid) + retrieved_user = await UserManager.check_user(user.user_id) assert retrieved_user is not None assert retrieved_user.rate_limits == partial_rate_limits.model_dump() @@ -183,13 +166,13 @@ async def test_rate_limits_json_partial_update(self, clean_database): '{user_rate_limit_hour}', '75' ) - WHERE userid = :userid + WHERE user_id = :user_id """) - await session.execute(stmt, {"userid": user.userid}) + await session.execute(stmt, {"user_id": user.user_id}) await session.commit() # Verify partial update worked - updated_user = await UserManager.check_user(user.userid) + updated_user = await UserManager.check_user(user.user_id) assert updated_user is not None expected_data = partial_rate_limits.model_dump() @@ -211,7 +194,7 @@ async def test_rate_limits_json_null_and_delete(self, clean_database): ) user = await UserManager.insert_user( - name="Delete Rate Limits User", rate_limits=rate_limits + user_id="Delete Rate Limits User", rate_limits=rate_limits ) # DELETE: Set rate_limits to NULL @@ -219,12 +202,14 @@ async def test_rate_limits_json_null_and_delete(self, clean_database): import sqlalchemy as sa async with get_db_session() as session: - stmt = sa.text("UPDATE users SET rate_limits = NULL WHERE userid = :userid") - await session.execute(stmt, {"userid": user.userid}) + stmt = sa.text( + "UPDATE users SET rate_limits = NULL WHERE user_id = :user_id" + ) + await session.execute(stmt, {"user_id": user.user_id}) await session.commit() # Verify NULL handling - null_user = await UserManager.check_user(user.userid) + null_user = await UserManager.check_user(user.user_id) assert null_user is not None assert null_user.rate_limits is None @@ -239,15 +224,15 @@ async def test_rate_limits_json_null_and_delete(self, clean_database): # First set some data new_data = {"user_rate_limit_day": 500, "web_search_rate_limit_day": 25} stmt = sa.text( - "UPDATE users SET rate_limits = :data WHERE userid = :userid" + "UPDATE users SET rate_limits = :data WHERE user_id = :user_id" ) await session.execute( - stmt, {"data": json.dumps(new_data), "userid": user.userid} + stmt, {"data": json.dumps(new_data), "user_id": user.user_id} ) await session.commit() # Verify data was set - updated_user = await UserManager.check_user(user.userid) + updated_user = await UserManager.check_user(user.user_id) assert updated_user is not None assert updated_user.rate_limits == new_data @@ -256,13 +241,13 @@ async def test_rate_limits_json_null_and_delete(self, clean_database): stmt = sa.text(""" UPDATE users SET rate_limits = rate_limits::jsonb - 'web_search_rate_limit_day' - WHERE userid = :userid + WHERE user_id = :user_id """) - await session.execute(stmt, {"userid": user.userid}) + await session.execute(stmt, {"user_id": user.user_id}) await session.commit() # Verify field was removed - final_user = await UserManager.check_user(user.userid) + final_user = await UserManager.check_user(user.user_id) expected_final_data = {"user_rate_limit_day": 500} assert final_user is not None assert final_user.rate_limits == expected_final_data @@ -293,15 +278,15 @@ async def test_rate_limits_json_validation_and_conversion(self, clean_database): for i, test_data in enumerate(test_cases): async with get_db_session() as session: stmt = sa.text( - "UPDATE users SET rate_limits = :data WHERE userid = :userid" + "UPDATE users SET rate_limits = :data WHERE user_id = :user_id" ) await session.execute( - stmt, {"data": json.dumps(test_data), "userid": user.userid} + stmt, {"data": json.dumps(test_data), "user_id": user.user_id} ) await session.commit() # Retrieve and verify - updated_user = await UserManager.check_user(user.userid) + updated_user = await UserManager.check_user(user.user_id) assert updated_user is not None assert updated_user.rate_limits == test_data @@ -327,11 +312,13 @@ async def test_rate_limits_json_validation_and_conversion(self, clean_database): # Test empty JSON object async with get_db_session() as session: - stmt = sa.text("UPDATE users SET rate_limits = '{}' WHERE userid = :userid") - await session.execute(stmt, {"userid": user.userid}) + stmt = sa.text( + "UPDATE users SET rate_limits = '{}' WHERE user_id = :user_id" + ) + await session.execute(stmt, {"user_id": user.user_id}) await session.commit() - empty_user = await UserManager.check_user(user.userid) + empty_user = await UserManager.check_user(user.user_id) assert empty_user is not None assert empty_user.rate_limits == {} empty_rate_limits_obj = empty_user.rate_limits_obj @@ -343,18 +330,18 @@ async def test_rate_limits_json_validation_and_conversion(self, clean_database): async with get_db_session() as session: # This should work as PostgreSQL JSONB validates JSON stmt = sa.text( - "UPDATE users SET rate_limits = :data WHERE userid = :userid" + "UPDATE users SET rate_limits = :data WHERE user_id = :user_id" ) await session.execute( stmt, { "data": '{"user_rate_limit_day": 5000}', # Valid JSON string - "userid": user.userid, + "user_id": user.user_id, }, ) await session.commit() - json_string_user = await UserManager.check_user(user.userid) + json_string_user = await UserManager.check_user(user.user_id) assert json_string_user is not None assert json_string_user.rate_limits == {"user_rate_limit_day": 5000} @@ -366,16 +353,15 @@ async def test_rate_limits_json_validation_and_conversion(self, clean_database): async def test_rate_limits_update_workflow(self, clean_database): """Test complete workflow: create user with no rate limits -> update rate limits -> verify update.""" # Step 1: Create user with NO rate limits - user = await UserManager.insert_user(name="Rate Limits Workflow User") + user_id = str(uuid.uuid4()) + user = await UserManager.insert_user(user_id=user_id) # Verify user was created with no rate limits - assert user.name == "Rate Limits Workflow User" - assert user.userid is not None - assert user.apikey is not None + assert user.user_id == user_id assert user.rate_limits is None # No rate limits initially # Step 2: Retrieve user and confirm no rate limits - retrieved_user = await UserManager.check_user(user.userid) + retrieved_user = await UserManager.check_user(user.user_id) assert retrieved_user is not None print(retrieved_user.to_pydantic()) assert retrieved_user is not None @@ -401,12 +387,12 @@ async def test_rate_limits_update_workflow(self, clean_database): # Step 4: Update the user's rate limits using the new function update_success = await UserManager.update_rate_limits( - user.userid, new_rate_limits + user.user_id, new_rate_limits ) assert update_success is True # Step 5: Retrieve user again and verify rate limits were updated - updated_user = await UserManager.check_user(user.userid) + updated_user = await UserManager.check_user(user.user_id) assert updated_user is not None assert updated_user.rate_limits is not None assert updated_user.rate_limits == new_rate_limits.model_dump() @@ -431,12 +417,12 @@ async def test_rate_limits_update_workflow(self, clean_database): ) partial_update_success = await UserManager.update_rate_limits( - user.userid, partial_rate_limits + user.user_id, partial_rate_limits ) assert partial_update_success is True # Step 8: Verify partial update worked - final_user = await UserManager.check_user(user.userid) + final_user = await UserManager.check_user(user.user_id) assert final_user is not None assert final_user.rate_limits == partial_rate_limits.model_dump() @@ -447,8 +433,8 @@ async def test_rate_limits_update_workflow(self, clean_database): # Other fields should have config defaults (not None due to get_effective_limits) # Step 9: Test error case - update non-existent user - fake_userid = "non-existent-user-id" + fake_user_id = "non-existent-user-id" error_update = await UserManager.update_rate_limits( - fake_userid, new_rate_limits + fake_user_id, new_rate_limits ) assert error_update is False diff --git a/tests/unit/nilai-common/conftest.py b/tests/unit/nilai-common/conftest.py new file mode 100644 index 00000000..58a8f41b --- /dev/null +++ b/tests/unit/nilai-common/conftest.py @@ -0,0 +1,19 @@ +import pytest +from testcontainers.redis import RedisContainer + + +@pytest.fixture(scope="session") +def redis_server(): + """Start a Redis container for testing.""" + container = RedisContainer() + container.start() + yield container + container.stop() + + +@pytest.fixture +def redis_host_port(redis_server): + """Get Redis host and port from the container.""" + host_ip = redis_server.get_container_host_ip() + host_port = redis_server.get_exposed_port(6379) + return host_ip, host_port diff --git a/tests/unit/nilai-common/test_discovery.py b/tests/unit/nilai-common/test_discovery.py index 34066125..363e99d0 100644 --- a/tests/unit/nilai-common/test_discovery.py +++ b/tests/unit/nilai-common/test_discovery.py @@ -1,22 +1,24 @@ import asyncio -from unittest.mock import AsyncMock, MagicMock, patch import pytest +import pytest_asyncio from nilai_common.api_models import ModelEndpoint, ModelMetadata from nilai_common.discovery import ModelServiceDiscovery -@pytest.fixture -def model_service_discovery(): - with patch("nilai_common.discovery.Etcd3Client") as MockClient: - mock_client = MockClient.return_value - discovery = ModelServiceDiscovery() - discovery.client = mock_client - yield discovery +@pytest_asyncio.fixture +async def model_service_discovery(redis_host_port): + """Create a ModelServiceDiscovery instance connected to the test Redis container.""" + host, port = redis_host_port + discovery = ModelServiceDiscovery(host=host, port=port, lease_ttl=60) + await discovery.initialize() + yield discovery + await discovery.close() @pytest.fixture def model_endpoint(): + """Create a sample model endpoint for testing.""" model_metadata = ModelMetadata( name="Test Model", version="1.0.0", @@ -34,63 +36,209 @@ def model_endpoint(): @pytest.mark.asyncio async def test_register_model(model_service_discovery, model_endpoint): - lease_mock = MagicMock() - model_service_discovery.client.lease.return_value = lease_mock + """Test registering a model in Redis.""" + key = await model_service_discovery.register_model(model_endpoint) - lease = await model_service_discovery.register_model(model_endpoint) + # Verify the key was created + assert key == f"/models/{model_endpoint.metadata.id}" - model_service_discovery.client.put.assert_called_once_with( - f"/models/{model_endpoint.metadata.id}", - model_endpoint.model_dump_json(), - lease=lease_mock, + # Verify we can retrieve it + retrieved_model = await model_service_discovery.get_model( + model_endpoint.metadata.id ) - assert lease == lease_mock + assert retrieved_model is not None + assert retrieved_model.metadata.id == model_endpoint.metadata.id + assert retrieved_model.url == model_endpoint.url + + # Cleanup + await model_service_discovery.unregister_model(model_endpoint.metadata.id) @pytest.mark.asyncio async def test_discover_models(model_service_discovery, model_endpoint): - model_service_discovery.client.get_prefix.return_value = [ - (model_endpoint.model_dump_json().encode("utf-8"), None) - ] + """Test discovering models from Redis.""" + # Register a model + await model_service_discovery.register_model(model_endpoint) + # Discover all models discovered_models = await model_service_discovery.discover_models() - assert len(discovered_models) == 1 + assert len(discovered_models) >= 1 assert model_endpoint.metadata.id in discovered_models - assert discovered_models[model_endpoint.metadata.id] == model_endpoint + assert discovered_models[model_endpoint.metadata.id].url == model_endpoint.url + + # Cleanup + await model_service_discovery.unregister_model(model_endpoint.metadata.id) @pytest.mark.asyncio -async def test_get_model(model_service_discovery, model_endpoint): - model_service_discovery.client.get.return_value = ( - model_endpoint.model_dump_json().encode("utf-8"), - None, +async def test_discover_models_with_filters(model_service_discovery): + """Test discovering models with name and feature filters.""" + # Create two different models + model_metadata_1 = ModelMetadata( + name="Image Model", + version="1.0.0", + description="Image classification model", + author="Test Author", + license="MIT", + source="https://github.com/test/model1", + supported_features=["image_classification"], + tool_support=False, + ) + model_endpoint_1 = ModelEndpoint( + url="http://image-model.example.com/predict", metadata=model_metadata_1 + ) + + model_metadata_2 = ModelMetadata( + name="Text Model", + version="1.0.0", + description="Text generation model", + author="Test Author", + license="MIT", + source="https://github.com/test/model2", + supported_features=["text_generation"], + tool_support=False, + ) + model_endpoint_2 = ModelEndpoint( + url="http://text-model.example.com/predict", metadata=model_metadata_2 + ) + + # Register both models + await model_service_discovery.register_model(model_endpoint_1) + await model_service_discovery.register_model(model_endpoint_2) + + # Filter by name + discovered_models = await model_service_discovery.discover_models(name="Image") + assert len(discovered_models) == 1 + assert model_endpoint_1.metadata.id in discovered_models + + # Filter by feature + discovered_models = await model_service_discovery.discover_models( + feature="text_generation" ) + assert len(discovered_models) == 1 + assert model_endpoint_2.metadata.id in discovered_models + + # Cleanup + await model_service_discovery.unregister_model(model_endpoint_1.metadata.id) + await model_service_discovery.unregister_model(model_endpoint_2.metadata.id) + + +@pytest.mark.asyncio +async def test_get_model(model_service_discovery, model_endpoint): + """Test getting a specific model by ID.""" + # Register a model + await model_service_discovery.register_model(model_endpoint) + # Get the model by ID model = await model_service_discovery.get_model(model_endpoint.metadata.id) - assert model == model_endpoint + assert model is not None + assert model.metadata.id == model_endpoint.metadata.id + assert model.url == model_endpoint.url + assert model.metadata.name == model_endpoint.metadata.name + + # Cleanup + await model_service_discovery.unregister_model(model_endpoint.metadata.id) + + +@pytest.mark.asyncio +async def test_get_nonexistent_model(model_service_discovery): + """Test getting a model that doesn't exist.""" + model = await model_service_discovery.get_model("nonexistent-model-id") + assert model is None @pytest.mark.asyncio async def test_unregister_model(model_service_discovery, model_endpoint): + """Test unregistering a model from Redis.""" + # Register a model + await model_service_discovery.register_model(model_endpoint) + + # Verify it exists + model = await model_service_discovery.get_model(model_endpoint.metadata.id) + assert model is not None + + # Unregister it await model_service_discovery.unregister_model(model_endpoint.metadata.id) - model_service_discovery.client.delete.assert_called_once_with( - f"/models/{model_endpoint.metadata.id}" + # Verify it's gone + model = await model_service_discovery.get_model(model_endpoint.metadata.id) + assert model is None + + +@pytest.mark.asyncio +async def test_keep_alive(model_service_discovery, model_endpoint): + """Test the keep_alive functionality that refreshes TTL.""" + # Register a model with a short TTL + short_ttl_discovery = ModelServiceDiscovery( + host=model_service_discovery.host, + port=model_service_discovery.port, + lease_ttl=2, # 2 second TTL + ) + await short_ttl_discovery.initialize() + + key = await short_ttl_discovery.register_model(model_endpoint) + + # Start keep_alive task + keep_alive_task = asyncio.create_task( + short_ttl_discovery.keep_alive(key, model_endpoint) ) + # Wait for more than one TTL period + await asyncio.sleep(3) + + # Model should still be there because keep_alive is refreshing it + model = await short_ttl_discovery.get_model(model_endpoint.metadata.id) + assert model is not None + + # Cancel the keep_alive task + keep_alive_task.cancel() + try: + await keep_alive_task + except asyncio.CancelledError: + pass + + # Wait for TTL to expire + await asyncio.sleep(3) + + # Model should be gone now + model = await short_ttl_discovery.get_model(model_endpoint.metadata.id) + assert model is None + + await short_ttl_discovery.close() + @pytest.mark.asyncio -async def test_keep_alive(model_service_discovery): - lease_mock = MagicMock() - lease_mock.refresh = AsyncMock() +async def test_keep_alive_with_stored_key(model_service_discovery, model_endpoint): + """Test keep_alive using the stored key from registration.""" + # Register a model with a short TTL + short_ttl_discovery = ModelServiceDiscovery( + host=model_service_discovery.host, + port=model_service_discovery.port, + lease_ttl=2, # 2 second TTL + ) + await short_ttl_discovery.initialize() + + await short_ttl_discovery.register_model(model_endpoint) + + # Start keep_alive task without passing the key (it should use the stored one) + keep_alive_task = asyncio.create_task( + short_ttl_discovery.keep_alive(model_endpoint=model_endpoint) + ) + + # Wait for more than one TTL period + await asyncio.sleep(3) - async def keep_alive_task(): - await model_service_discovery.keep_alive(lease_mock) + # Model should still be there + model = await short_ttl_discovery.get_model(model_endpoint.metadata.id) + assert model is not None - task = asyncio.create_task(keep_alive_task()) - await asyncio.sleep(0.1) - task.cancel() + # Cancel the keep_alive task + keep_alive_task.cancel() + try: + await keep_alive_task + except asyncio.CancelledError: + pass - lease_mock.refresh.assert_called() + await short_ttl_discovery.close() diff --git a/tests/unit/nilai_api/__init__.py b/tests/unit/nilai_api/__init__.py index 0be52613..7cbc1237 100644 --- a/tests/unit/nilai_api/__init__.py +++ b/tests/unit/nilai_api/__init__.py @@ -21,11 +21,11 @@ def generate_api_key(self) -> str: async def insert_user(self, name: str, email: str) -> Dict[str, str]: """Insert a new user into the mock database.""" - userid = self.generate_user_id() + user_id = self.generate_user_id() apikey = self.generate_api_key() user_data = { - "userid": userid, + "user_id": user_id, "name": name, "email": email, "apikey": apikey, @@ -36,34 +36,34 @@ async def insert_user(self, name: str, email: str) -> Dict[str, str]: "last_activity": None, } - self.users[userid] = user_data - return {"userid": userid, "apikey": apikey} + self.users[user_id] = user_data + return {"user_id": user_id, "apikey": apikey} async def check_api_key(self, api_key: str) -> Optional[dict]: """Validate an API key in the mock database.""" for user in self.users.values(): if user["apikey"] == api_key: - return {"name": user["name"], "userid": user["userid"]} + return {"name": user["name"], "user_id": user["user_id"]} return None async def update_token_usage( - self, userid: str, prompt_tokens: int, completion_tokens: int + self, user_id: str, prompt_tokens: int, completion_tokens: int ): """Update token usage for a specific user.""" - if userid in self.users: - user = self.users[userid] + if user_id in self.users: + user = self.users[user_id] user["prompt_tokens"] += prompt_tokens user["completion_tokens"] += completion_tokens user["queries"] += 1 user["last_activity"] = datetime.now(timezone.utc) async def log_query( - self, userid: str, model: str, prompt_tokens: int, completion_tokens: int + self, user_id: str, model: str, prompt_tokens: int, completion_tokens: int ): """Log a user's query in the mock database.""" query_log = { "id": self._next_query_log_id, - "userid": userid, + "user_id": user_id, "query_timestamp": datetime.now(timezone.utc), "model": model, "prompt_tokens": prompt_tokens, @@ -74,9 +74,9 @@ async def log_query( self.query_logs[self._next_query_log_id] = query_log self._next_query_log_id += 1 - async def get_token_usage(self, userid: str) -> Optional[Dict[str, Any]]: + async def get_token_usage(self, user_id: str) -> Optional[Dict[str, Any]]: """Get token usage for a specific user.""" - user = self.users.get(userid) + user = self.users.get(user_id) if user: return { "prompt_tokens": user["prompt_tokens"], @@ -90,9 +90,9 @@ async def get_all_users(self) -> Optional[List[Dict[str, Any]]]: """Retrieve all users from the mock database.""" return list(self.users.values()) if self.users else None - async def get_user_token_usage(self, userid: str) -> Optional[Dict[str, int]]: + async def get_user_token_usage(self, user_id: str) -> Optional[Dict[str, int]]: """Retrieve total token usage for a user.""" - user = self.users.get(userid) + user = self.users.get(user_id) if user: return { "prompt_tokens": user["prompt_tokens"], diff --git a/tests/unit/nilai_api/auth/test_auth.py b/tests/unit/nilai_api/auth/test_auth.py index 591c447a..47559272 100644 --- a/tests/unit/nilai_api/auth/test_auth.py +++ b/tests/unit/nilai_api/auth/test_auth.py @@ -1,10 +1,8 @@ -from datetime import datetime, timezone import logging from unittest.mock import MagicMock from nilai_api.db.users import RateLimits import pytest -from fastapi import HTTPException from fastapi.security import HTTPAuthorizationCredentials from nilai_api.config import CONFIG as config @@ -14,13 +12,9 @@ @pytest.fixture -def mock_user_manager(mocker): - from nilai_api.db.users import UserManager - - """Fixture to mock UserManager methods.""" - mocker.patch.object(UserManager, "check_api_key") - mocker.patch.object(UserManager, "update_last_activity") - return UserManager +def mock_validate_credential(mocker): + """Fixture to mock validate_credential function.""" + return mocker.patch("nilai_api.auth.strategies.validate_credential") @pytest.fixture @@ -28,14 +22,7 @@ def mock_user_model(): from nilai_api.db.users import UserModel mock = MagicMock(spec=UserModel) - mock.name = "Test User" - mock.userid = "test-user-id" - mock.apikey = "test-api-key" - mock.prompt_tokens = 0 - mock.completion_tokens = 0 - mock.queries = 0 - mock.signup_date = datetime.now(timezone.utc) - mock.last_activity = datetime.now(timezone.utc) + mock.user_id = "test-user-id" mock.rate_limits = RateLimits().get_effective_limits().model_dump_json() mock.rate_limits_obj = RateLimits().get_effective_limits() return mock @@ -49,53 +36,38 @@ def mock_user_data(mock_user_model): return UserData.from_sqlalchemy(mock_user_model) -@pytest.fixture -def mock_auth_info(): - from nilai_api.auth import AuthenticationInfo - - mock = MagicMock(spec=AuthenticationInfo) - mock.user = mock_user_data - return mock - - @pytest.mark.asyncio -async def test_get_auth_info_valid_token( - mock_user_manager, mock_auth_info, mock_user_model -): +async def test_get_auth_info_valid_token(mock_validate_credential, mock_user_model): from nilai_api.auth import get_auth_info """Test get_auth_info with a valid token.""" - mock_user_manager.check_api_key.return_value = mock_user_model + mock_validate_credential.return_value = mock_user_model credentials = HTTPAuthorizationCredentials( scheme="Bearer", credentials="valid-token" ) auth_info = await get_auth_info(credentials) print(auth_info) - assert auth_info.user.name == "Test User", ( - f"Expected Test User but got {auth_info.user.name}" - ) - assert auth_info.user.userid == "test-user-id", ( - f"Expected test-user-id but got {auth_info.user.userid}" + + assert auth_info.user.user_id == "test-user-id", ( + f"Expected test-user-id but got {auth_info.user.user_id}" ) @pytest.mark.asyncio -async def test_get_auth_info_invalid_token(mock_user_manager): +async def test_get_auth_info_invalid_token(mock_validate_credential): from nilai_api.auth import get_auth_info + from nilai_api.auth.common import AuthenticationError """Test get_auth_info with an invalid token.""" - mock_user_manager.check_api_key.return_value = None + mock_validate_credential.side_effect = AuthenticationError("Credential not found") credentials = HTTPAuthorizationCredentials( scheme="Bearer", credentials="invalid-token" ) - with pytest.raises(HTTPException) as exc_info: + with pytest.raises(AuthenticationError) as exc_info: auth_infor = await get_auth_info(credentials) print(auth_infor) print(exc_info) - assert exc_info.value.status_code == 401, ( - f"Expected status code 401 but got {exc_info.value.status_code}" - ) - assert exc_info.value.detail == "Missing or invalid API key", ( - f"Expected Missing or invalid API key but got {exc_info.value.detail}" + assert "Credential not found" in str(exc_info.value.detail), ( + f"Expected 'Credential not found' but got {exc_info.value.detail}" ) diff --git a/tests/unit/nilai_api/auth/test_strategies.py b/tests/unit/nilai_api/auth/test_strategies.py index 0c169f53..5b65c5b0 100644 --- a/tests/unit/nilai_api/auth/test_strategies.py +++ b/tests/unit/nilai_api/auth/test_strategies.py @@ -1,7 +1,6 @@ import pytest from unittest.mock import patch, MagicMock from datetime import datetime, timezone, timedelta -from fastapi import HTTPException from nilai_api.auth.strategies import api_key_strategy, nuc_strategy from nilai_api.auth.common import AuthenticationInfo, PromptDocument @@ -15,14 +14,7 @@ class TestAuthStrategies: def mock_user_model(self): """Mock UserModel fixture""" mock = MagicMock(spec=UserModel) - mock.name = "Test User" - mock.userid = "test-user-id" - mock.apikey = "test-api-key" - mock.prompt_tokens = 0 - mock.completion_tokens = 0 - mock.queries = 0 - mock.signup_date = datetime.now(timezone.utc) - mock.last_activity = datetime.now(timezone.utc) + mock.user_id = "test-user-id" mock.rate_limits = RateLimits().get_effective_limits().model_dump_json() mock.rate_limits_obj = RateLimits().get_effective_limits() return mock @@ -37,27 +29,26 @@ def mock_prompt_document(self): @pytest.mark.asyncio async def test_api_key_strategy_success(self, mock_user_model): """Test successful API key authentication""" - with patch("nilai_api.auth.strategies.UserManager.check_api_key") as mock_check: - mock_check.return_value = mock_user_model + with patch("nilai_api.auth.strategies.validate_credential") as mock_validate: + mock_validate.return_value = mock_user_model result = await api_key_strategy("test-api-key") assert isinstance(result, AuthenticationInfo) - assert result.user.name == "Test User" assert result.token_rate_limit is None assert result.prompt_document is None + mock_validate.assert_called_once_with("test-api-key", is_public=False) @pytest.mark.asyncio async def test_api_key_strategy_invalid_key(self): """Test API key authentication with invalid key""" - with patch("nilai_api.auth.strategies.UserManager.check_api_key") as mock_check: - mock_check.return_value = None + from nilai_api.auth.common import AuthenticationError - with pytest.raises(HTTPException) as exc_info: - await api_key_strategy("invalid-key") + with patch("nilai_api.auth.strategies.validate_credential") as mock_validate: + mock_validate.side_effect = AuthenticationError("Credential not found") - assert exc_info.value.status_code == 401 - assert "Missing or invalid API key" in str(exc_info.value.detail) + with pytest.raises(AuthenticationError, match="Credential not found"): + await api_key_strategy("invalid-key") @pytest.mark.asyncio async def test_nuc_strategy_existing_user_with_prompt_document( @@ -65,7 +56,7 @@ async def test_nuc_strategy_existing_user_with_prompt_document( ): """Test NUC authentication with existing user and prompt document""" with ( - patch("nilai_api.auth.strategies.validate_nuc") as mock_validate, + patch("nilai_api.auth.strategies.validate_nuc") as mock_validate_nuc, patch( "nilai_api.auth.strategies.get_token_rate_limit" ) as mock_get_rate_limit, @@ -73,23 +64,27 @@ async def test_nuc_strategy_existing_user_with_prompt_document( "nilai_api.auth.strategies.get_token_prompt_document" ) as mock_get_prompt_doc, patch( - "nilai_api.auth.strategies.UserManager.check_user" - ) as mock_check_user, + "nilai_api.auth.strategies.validate_credential" + ) as mock_validate_credential, ): - mock_validate.return_value = ("subscription_holder", "user_id") + mock_validate_nuc.return_value = ("subscription_holder", "user_id") mock_get_rate_limit.return_value = None mock_get_prompt_doc.return_value = mock_prompt_document - mock_check_user.return_value = mock_user_model + mock_validate_credential.return_value = mock_user_model result = await nuc_strategy("nuc-token") assert isinstance(result, AuthenticationInfo) - assert result.user.name == "Test User" assert result.token_rate_limit is None assert result.prompt_document == mock_prompt_document + mock_validate_credential.assert_called_once_with( + "subscription_holder", is_public=True + ) @pytest.mark.asyncio - async def test_nuc_strategy_new_user_with_token_limits(self, mock_prompt_document): + async def test_nuc_strategy_new_user_with_token_limits( + self, mock_prompt_document, mock_user_model + ): """Test NUC authentication creating new user with token limits""" from nilai_api.auth.nuc_helpers.usage import TokenRateLimits, TokenRateLimit @@ -104,7 +99,7 @@ async def test_nuc_strategy_new_user_with_token_limits(self, mock_prompt_documen ) with ( - patch("nilai_api.auth.strategies.validate_nuc") as mock_validate, + patch("nilai_api.auth.strategies.validate_nuc") as mock_validate_nuc, patch( "nilai_api.auth.strategies.get_token_rate_limit" ) as mock_get_rate_limit, @@ -112,30 +107,28 @@ async def test_nuc_strategy_new_user_with_token_limits(self, mock_prompt_documen "nilai_api.auth.strategies.get_token_prompt_document" ) as mock_get_prompt_doc, patch( - "nilai_api.auth.strategies.UserManager.check_user" - ) as mock_check_user, - patch( - "nilai_api.auth.strategies.UserManager.insert_user_model" - ) as mock_insert, + "nilai_api.auth.strategies.validate_credential" + ) as mock_validate_credential, ): - mock_validate.return_value = ("subscription_holder", "new_user_id") + mock_validate_nuc.return_value = ("subscription_holder", "new_user_id") mock_get_rate_limit.return_value = mock_token_limits mock_get_prompt_doc.return_value = mock_prompt_document - mock_check_user.return_value = None - mock_insert.return_value = None + mock_validate_credential.return_value = mock_user_model result = await nuc_strategy("nuc-token") assert isinstance(result, AuthenticationInfo) assert result.token_rate_limit == mock_token_limits assert result.prompt_document == mock_prompt_document - mock_insert.assert_called_once() + mock_validate_credential.assert_called_once_with( + "subscription_holder", is_public=True + ) @pytest.mark.asyncio async def test_nuc_strategy_no_prompt_document(self, mock_user_model): """Test NUC authentication when no prompt document is found""" with ( - patch("nilai_api.auth.strategies.validate_nuc") as mock_validate, + patch("nilai_api.auth.strategies.validate_nuc") as mock_validate_nuc, patch( "nilai_api.auth.strategies.get_token_rate_limit" ) as mock_get_rate_limit, @@ -143,18 +136,17 @@ async def test_nuc_strategy_no_prompt_document(self, mock_user_model): "nilai_api.auth.strategies.get_token_prompt_document" ) as mock_get_prompt_doc, patch( - "nilai_api.auth.strategies.UserManager.check_user" - ) as mock_check_user, + "nilai_api.auth.strategies.validate_credential" + ) as mock_validate_credential, ): - mock_validate.return_value = ("subscription_holder", "user_id") + mock_validate_nuc.return_value = ("subscription_holder", "user_id") mock_get_rate_limit.return_value = None mock_get_prompt_doc.return_value = None - mock_check_user.return_value = mock_user_model + mock_validate_credential.return_value = mock_user_model result = await nuc_strategy("nuc-token") assert isinstance(result, AuthenticationInfo) - assert result.user.name == "Test User" assert result.token_rate_limit is None assert result.prompt_document is None @@ -171,7 +163,7 @@ async def test_nuc_strategy_validation_error(self): async def test_nuc_strategy_get_prompt_document_error(self, mock_user_model): """Test NUC authentication when get_token_prompt_document fails""" with ( - patch("nilai_api.auth.strategies.validate_nuc") as mock_validate, + patch("nilai_api.auth.strategies.validate_nuc") as mock_validate_nuc, patch( "nilai_api.auth.strategies.get_token_rate_limit" ) as mock_get_rate_limit, @@ -179,15 +171,15 @@ async def test_nuc_strategy_get_prompt_document_error(self, mock_user_model): "nilai_api.auth.strategies.get_token_prompt_document" ) as mock_get_prompt_doc, patch( - "nilai_api.auth.strategies.UserManager.check_user" - ) as mock_check_user, + "nilai_api.auth.strategies.validate_credential" + ) as mock_validate_credential, ): - mock_validate.return_value = ("subscription_holder", "user_id") + mock_validate_nuc.return_value = ("subscription_holder", "user_id") mock_get_rate_limit.return_value = None mock_get_prompt_doc.side_effect = Exception( "Prompt document extraction failed" ) - mock_check_user.return_value = mock_user_model + mock_validate_credential.return_value = mock_user_model # The function should let the exception bubble up or handle it gracefully # Based on the diff, it looks like it doesn't catch exceptions from get_token_prompt_document @@ -200,29 +192,22 @@ async def test_all_strategies_return_authentication_info_with_prompt_document_fi ): """Test that all strategies return AuthenticationInfo with prompt_document field""" mock_user_model = MagicMock(spec=UserModel) - mock_user_model.name = "Test" - mock_user_model.userid = "test" - mock_user_model.apikey = "test" - mock_user_model.prompt_tokens = 0 - mock_user_model.completion_tokens = 0 - mock_user_model.queries = 0 - mock_user_model.signup_date = datetime.now(timezone.utc) - mock_user_model.last_activity = datetime.now(timezone.utc) + mock_user_model.user_id = "test" mock_user_model.rate_limits = ( RateLimits().get_effective_limits().model_dump_json() ) mock_user_model.rate_limits_obj = RateLimits().get_effective_limits() # Test API key strategy - with patch("nilai_api.auth.strategies.UserManager.check_api_key") as mock_check: - mock_check.return_value = mock_user_model + with patch("nilai_api.auth.strategies.validate_credential") as mock_validate: + mock_validate.return_value = mock_user_model result = await api_key_strategy("test-key") assert hasattr(result, "prompt_document") assert result.prompt_document is None # Test NUC strategy with ( - patch("nilai_api.auth.strategies.validate_nuc") as mock_validate, + patch("nilai_api.auth.strategies.validate_nuc") as mock_validate_nuc, patch( "nilai_api.auth.strategies.get_token_rate_limit" ) as mock_get_rate_limit, @@ -230,13 +215,13 @@ async def test_all_strategies_return_authentication_info_with_prompt_document_fi "nilai_api.auth.strategies.get_token_prompt_document" ) as mock_get_prompt_doc, patch( - "nilai_api.auth.strategies.UserManager.check_user" - ) as mock_check_user, + "nilai_api.auth.strategies.validate_credential" + ) as mock_validate_credential, ): - mock_validate.return_value = ("subscription_holder", "user_id") + mock_validate_nuc.return_value = ("subscription_holder", "user_id") mock_get_rate_limit.return_value = None mock_get_prompt_doc.return_value = None - mock_check_user.return_value = mock_user_model + mock_validate_credential.return_value = mock_user_model result = await nuc_strategy("nuc-token") assert hasattr(result, "prompt_document") diff --git a/tests/unit/nilai_api/routers/test_chat_completions_private.py b/tests/unit/nilai_api/routers/test_chat_completions_private.py index 8441a189..4c1f30b2 100644 --- a/tests/unit/nilai_api/routers/test_chat_completions_private.py +++ b/tests/unit/nilai_api/routers/test_chat_completions_private.py @@ -4,16 +4,12 @@ import pytest from fastapi.testclient import TestClient - from nilai_api.db.users import RateLimits, UserModel +from nilai_api.state import state from nilai_common import AttestationReport, Source -from nilai_api.state import state -from ... import ( - model_endpoint, - model_metadata, - response as RESPONSE, -) +from ... import model_endpoint, model_metadata +from ... import response as RESPONSE @pytest.mark.asyncio @@ -24,15 +20,7 @@ async def test_runs_in_a_loop(): @pytest.fixture def mock_user(): mock = MagicMock(spec=UserModel) - mock.userid = "test-user-id" - mock.name = "Test User" - mock.apikey = "test-api-key" - mock.prompt_tokens = 100 - mock.completion_tokens = 50 - mock.total_tokens = 150 - mock.completion_tokens_details = None - mock.prompt_tokens_details = None - mock.queries = 10 + mock.user_id = "test-user-id" mock.rate_limits = RateLimits().get_effective_limits().model_dump_json() mock.rate_limits_obj = RateLimits().get_effective_limits() return mock @@ -40,64 +28,32 @@ def mock_user(): @pytest.fixture def mock_user_manager(mock_user, mocker): - from nilai_api.db.users import UserManager from nilai_api.db.logs import QueryLogManager + from nilai_api.db.users import UserManager + from nilai_common import Usage + # Mock QueryLogManager for usage tracking mocker.patch.object( - UserManager, - "get_token_usage", - return_value={ - "prompt_tokens": 100, - "completion_tokens": 50, - "total_tokens": 150, - "queries": 10, - }, - ) - mocker.patch.object(UserManager, "update_token_usage") - mocker.patch.object( - UserManager, + QueryLogManager, "get_user_token_usage", - return_value={ - "prompt_tokens": 100, - "completion_tokens": 50, - "total_tokens": 150, - "completion_tokens_details": None, - "prompt_tokens_details": None, - "queries": 10, - }, - ) - mocker.patch.object( - UserManager, - "insert_user", - return_value={ - "userid": "test-user-id", - "apikey": "test-api-key", - "rate_limits": RateLimits().get_effective_limits().model_dump_json(), - }, + new_callable=AsyncMock, + return_value=Usage( + prompt_tokens=100, + completion_tokens=50, + total_tokens=150, + completion_tokens_details=None, + prompt_tokens_details=None, + ), ) - mocker.patch.object( - UserManager, - "check_api_key", + mocker.patch.object(QueryLogManager, "log_query", new_callable=AsyncMock) + + # Mock validate_credential for authentication + mocker.patch( + "nilai_api.auth.strategies.validate_credential", + new_callable=AsyncMock, return_value=mock_user, ) - mocker.patch.object( - UserManager, - "get_all_users", - return_value=[ - { - "userid": "test-user-id", - "apikey": "test-api-key", - "rate_limits": RateLimits().get_effective_limits().model_dump_json(), - }, - { - "userid": "test-user-id-2", - "apikey": "test-api-key", - "rate_limits": RateLimits().get_effective_limits().model_dump_json(), - }, - ], - ) - mocker.patch.object(QueryLogManager, "log_query") - mocker.patch.object(UserManager, "update_last_activity") + return UserManager @@ -108,21 +64,21 @@ def mock_state(mocker): # Create a mock discovery service that returns the expected models mock_discovery_service = mocker.Mock() + mock_discovery_service.initialize = AsyncMock() mock_discovery_service.discover_models = AsyncMock(return_value=expected_models) + mock_discovery_service.get_model = AsyncMock(return_value=model_endpoint) # Create a mock AppState mocker.patch.object(state, "discovery_service", mock_discovery_service) + mocker.patch.object(state, "_discovery_initialized", False) # Patch other attributes mocker.patch.object(state, "b64_public_key", "test-verifying-key") - # Patch get_model method - mocker.patch.object(state, "get_model", return_value=model_endpoint) - # Patch get_attestation method attestation_response = AttestationReport( - verifying_key="test-verifying-key", nonce="0" * 64, + verifying_key="test-verifying-key", cpu_attestation="test-cpu-attestation", gpu_attestation="test-gpu-attestation", ) @@ -178,7 +134,6 @@ def test_get_usage(mock_user, mock_user_manager, mock_state, client): "total_tokens": 150, "completion_tokens_details": None, "prompt_tokens_details": None, - "queries": 10, } @@ -225,6 +180,7 @@ def test_chat_completion(mock_user, mock_state, mock_user_manager, mocker, clien "nilai_api.routers.endpoints.chat.handle_tool_workflow", return_value=(response_data, 0, 0), ) + mocker.patch("nilai_api.db.logs.QueryLogContext.commit", new_callable=AsyncMock) response = client.post( "/v1/chat/completions", json={ @@ -265,6 +221,7 @@ def test_chat_completion_stream_includes_sources( "nilai_api.routers.endpoints.chat.handle_web_search", new=AsyncMock(return_value=mock_web_search_result), ) + mocker.patch("nilai_api.db.logs.QueryLogContext.commit", new_callable=AsyncMock) class MockChunk: def __init__(self, data, usage=None): diff --git a/tests/unit/nilai_api/routers/test_nildb_endpoints.py b/tests/unit/nilai_api/routers/test_nildb_endpoints.py index b54b664c..ff1ecdd6 100644 --- a/tests/unit/nilai_api/routers/test_nildb_endpoints.py +++ b/tests/unit/nilai_api/routers/test_nildb_endpoints.py @@ -7,7 +7,6 @@ from nilai_api.handlers.nildb.api_model import ( PromptDelegationToken, ) -from datetime import datetime, timezone from nilai_common import ResponseRequest @@ -18,14 +17,7 @@ class TestNilDBEndpoints: def mock_subscription_owner_user(self): """Mock user data for subscription owner""" mock_user_model = MagicMock(spec=UserModel) - mock_user_model.name = "Subscription Owner" - mock_user_model.userid = "owner-id" - mock_user_model.apikey = "owner-id" # Same as userid for subscription owner - mock_user_model.prompt_tokens = 0 - mock_user_model.completion_tokens = 0 - mock_user_model.queries = 0 - mock_user_model.signup_date = datetime.now(timezone.utc) - mock_user_model.last_activity = datetime.now(timezone.utc) + mock_user_model.user_id = "owner-id" mock_user_model.rate_limits = ( RateLimits().get_effective_limits().model_dump_json() ) @@ -37,14 +29,7 @@ def mock_subscription_owner_user(self): def mock_regular_user(self): """Mock user data for regular user (not subscription owner)""" mock_user_model = MagicMock(spec=UserModel) - mock_user_model.name = "Regular User" - mock_user_model.userid = "user-id" - mock_user_model.apikey = "different-api-key" # Different from userid - mock_user_model.prompt_tokens = 0 - mock_user_model.completion_tokens = 0 - mock_user_model.queries = 0 - mock_user_model.signup_date = datetime.now(timezone.utc) - mock_user_model.last_activity = datetime.now(timezone.utc) + mock_user_model.user_id = "user-id" mock_user_model.rate_limits = ( RateLimits().get_effective_limits().model_dump_json() ) @@ -99,21 +84,25 @@ async def test_get_prompt_store_delegation_success( mock_get_delegation.assert_called_once_with("user-123") @pytest.mark.asyncio - async def test_get_prompt_store_delegation_forbidden_regular_user( - self, mock_auth_info_regular_user + async def test_get_prompt_store_delegation_success_regular_user( + self, mock_auth_info_regular_user, mock_prompt_delegation_token ): - """Test delegation token request by regular user (not subscription owner)""" + """Test delegation token request by regular user (endpoint no longer checks subscription ownership)""" from nilai_api.routers.private import get_prompt_store_delegation - request = "user-123" + with patch( + "nilai_api.routers.private.get_nildb_delegation_token" + ) as mock_get_delegation: + mock_get_delegation.return_value = mock_prompt_delegation_token - with pytest.raises(HTTPException) as exc_info: - await get_prompt_store_delegation(request, mock_auth_info_regular_user) + request = "user-123" - assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN - assert "Prompt storage is reserved to subscription owners" in str( - exc_info.value.detail - ) + result = await get_prompt_store_delegation( + request, mock_auth_info_regular_user + ) + + assert isinstance(result, PromptDelegationToken) + assert result.token == "delegation_token_123" @pytest.mark.asyncio async def test_get_prompt_store_delegation_handler_error( @@ -150,7 +139,7 @@ async def test_chat_completion_with_prompt_document_injection(self): ) mock_user = MagicMock() - mock_user.userid = "test-user-id" + mock_user.user_id = "test-user-id" mock_user.name = "Test User" mock_user.apikey = "test-api-key" mock_user.rate_limits = RateLimits().get_effective_limits() @@ -163,6 +152,14 @@ async def test_chat_completion_with_prompt_document_injection(self): mock_meter = MagicMock() mock_meter.set_response = MagicMock() + # Mock log context + mock_log_ctx = MagicMock() + mock_log_ctx.set_user = MagicMock() + mock_log_ctx.set_model = MagicMock() + mock_log_ctx.set_request_params = MagicMock() + mock_log_ctx.start_model_timing = MagicMock() + mock_log_ctx.end_model_timing = MagicMock() + request = ChatRequest( model="test-model", messages=[{"role": "user", "content": "Hello"}] ) @@ -179,12 +176,6 @@ async def test_chat_completion_with_prompt_document_injection(self): patch( "nilai_api.routers.endpoints.chat.handle_web_search" ) as mock_handle_web_search, - patch( - "nilai_api.routers.endpoints.chat.UserManager.update_token_usage" - ) as mock_update_usage, - patch( - "nilai_api.routers.endpoints.chat.QueryLogManager.log_query" - ) as mock_log_query, patch( "nilai_api.routers.endpoints.chat.handle_tool_workflow" ) as mock_handle_tool_workflow, @@ -205,10 +196,6 @@ async def test_chat_completion_with_prompt_document_injection(self): mock_web_search_result.sources = [] mock_handle_web_search.return_value = mock_web_search_result - # Mock async database operations - mock_update_usage.return_value = None - mock_log_query.return_value = None - # Mock OpenAI client mock_client_instance = MagicMock() mock_response = MagicMock() @@ -250,7 +237,10 @@ async def test_chat_completion_with_prompt_document_injection(self): # Call the function (this will test the prompt injection logic) await chat_completion( - req=request, auth_info=mock_auth_info, meter=mock_meter + req=request, + auth_info=mock_auth_info, + meter=mock_meter, + log_ctx=mock_log_ctx, ) mock_get_prompt.assert_called_once_with(mock_prompt_document) @@ -266,9 +256,7 @@ async def test_chat_completion_prompt_document_extraction_error(self): ) mock_user = MagicMock() - mock_user.userid = "test-user-id" - mock_user.name = "Test User" - mock_user.apikey = "test-api-key" + mock_user.user_id = "test-user-id" mock_user.rate_limits = RateLimits().get_effective_limits() mock_auth_info = AuthenticationInfo( @@ -279,6 +267,12 @@ async def test_chat_completion_prompt_document_extraction_error(self): mock_meter = MagicMock() mock_meter.set_response = MagicMock() + # Mock log context + mock_log_ctx = MagicMock() + mock_log_ctx.set_user = MagicMock() + mock_log_ctx.set_model = MagicMock() + mock_log_ctx.set_request_params = MagicMock() + request = ChatRequest( model="test-model", messages=[{"role": "user", "content": "Hello"}] ) @@ -300,7 +294,10 @@ async def test_chat_completion_prompt_document_extraction_error(self): with pytest.raises(HTTPException) as exc_info: await chat_completion( - req=request, auth_info=mock_auth_info, meter=mock_meter + req=request, + auth_info=mock_auth_info, + meter=mock_meter, + log_ctx=mock_log_ctx, ) assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN @@ -316,9 +313,7 @@ async def test_chat_completion_without_prompt_document(self): from nilai_common import ChatRequest mock_user = MagicMock() - mock_user.userid = "test-user-id" - mock_user.name = "Test User" - mock_user.apikey = "test-api-key" + mock_user.user_id = "test-user-id" mock_user.rate_limits = RateLimits().get_effective_limits() mock_auth_info = AuthenticationInfo( @@ -331,6 +326,14 @@ async def test_chat_completion_without_prompt_document(self): mock_meter = MagicMock() mock_meter.set_response = MagicMock() + # Mock log context + mock_log_ctx = MagicMock() + mock_log_ctx.set_user = MagicMock() + mock_log_ctx.set_model = MagicMock() + mock_log_ctx.set_request_params = MagicMock() + mock_log_ctx.start_model_timing = MagicMock() + mock_log_ctx.end_model_timing = MagicMock() + request = ChatRequest( model="test-model", messages=[{"role": "user", "content": "Hello"}] ) @@ -347,12 +350,6 @@ async def test_chat_completion_without_prompt_document(self): patch( "nilai_api.routers.endpoints.chat.handle_web_search" ) as mock_handle_web_search, - patch( - "nilai_api.routers.endpoints.chat.UserManager.update_token_usage" - ) as mock_update_usage, - patch( - "nilai_api.routers.endpoints.chat.QueryLogManager.log_query" - ) as mock_log_query, patch( "nilai_api.routers.endpoints.chat.handle_tool_workflow" ) as mock_handle_tool_workflow, @@ -371,10 +368,6 @@ async def test_chat_completion_without_prompt_document(self): mock_web_search_result.sources = [] mock_handle_web_search.return_value = mock_web_search_result - # Mock async database operations - mock_update_usage.return_value = None - mock_log_query.return_value = None - # Mock OpenAI client mock_client_instance = MagicMock() mock_response = MagicMock() @@ -412,7 +405,10 @@ async def test_chat_completion_without_prompt_document(self): # Call the function await chat_completion( - req=request, auth_info=mock_auth_info, meter=mock_meter + req=request, + auth_info=mock_auth_info, + meter=mock_meter, + log_ctx=mock_log_ctx, ) # Should not call get_prompt_from_nildb when no prompt document @@ -428,7 +424,7 @@ async def test_responses_with_prompt_document_injection(self): ) mock_user = MagicMock() - mock_user.userid = "test-user-id" + mock_user.user_id = "test-user-id" mock_user.name = "Test User" mock_user.apikey = "test-api-key" mock_user.rate_limits = RateLimits().get_effective_limits() @@ -468,12 +464,6 @@ async def test_responses_with_prompt_document_injection(self): patch( "nilai_api.routers.endpoints.responses.state.get_model" ) as mock_get_model, - patch( - "nilai_api.routers.endpoints.responses.UserManager.update_token_usage" - ) as mock_update_usage, - patch( - "nilai_api.routers.endpoints.responses.QueryLogManager.log_query" - ) as mock_log_query, patch( "nilai_api.routers.endpoints.responses.handle_responses_tool_workflow" ) as mock_handle_tool_workflow, @@ -486,9 +476,6 @@ async def test_responses_with_prompt_document_injection(self): mock_model_endpoint.metadata.multimodal_support = True mock_get_model.return_value = mock_model_endpoint - mock_update_usage.return_value = None - mock_log_query.return_value = None - mock_client_instance = MagicMock() mock_response = MagicMock() mock_response.model_dump.return_value = response_payload @@ -502,8 +489,13 @@ async def test_responses_with_prompt_document_injection(self): mock_meter = MagicMock() mock_meter.set_response = MagicMock() + mock_log_ctx = MagicMock() + await create_response( - req=request, auth_info=mock_auth_info, meter=mock_meter + req=request, + auth_info=mock_auth_info, + meter=mock_meter, + log_ctx=mock_log_ctx, ) mock_get_prompt.assert_called_once_with(mock_prompt_document) @@ -518,7 +510,7 @@ async def test_responses_prompt_document_extraction_error(self): ) mock_user = MagicMock() - mock_user.userid = "test-user-id" + mock_user.user_id = "test-user-id" mock_user.name = "Test User" mock_user.apikey = "test-api-key" mock_user.rate_limits = RateLimits().get_effective_limits() @@ -545,8 +537,16 @@ async def test_responses_prompt_document_extraction_error(self): mock_get_prompt.side_effect = Exception("Unable to extract prompt") + mock_meter = MagicMock() + mock_log_ctx = MagicMock() + with pytest.raises(HTTPException) as exc_info: - await create_response(req=request, auth_info=mock_auth_info) + await create_response( + req=request, + auth_info=mock_auth_info, + meter=mock_meter, + log_ctx=mock_log_ctx, + ) assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN assert ( @@ -560,7 +560,7 @@ async def test_responses_without_prompt_document(self): from nilai_api.routers.endpoints.responses import create_response mock_user = MagicMock() - mock_user.userid = "test-user-id" + mock_user.user_id = "test-user-id" mock_user.name = "Test User" mock_user.apikey = "test-api-key" mock_user.rate_limits = RateLimits().get_effective_limits() @@ -602,12 +602,6 @@ async def test_responses_without_prompt_document(self): patch( "nilai_api.routers.endpoints.responses.state.get_model" ) as mock_get_model, - patch( - "nilai_api.routers.endpoints.responses.UserManager.update_token_usage" - ) as mock_update_usage, - patch( - "nilai_api.routers.endpoints.responses.QueryLogManager.log_query" - ) as mock_log_query, patch( "nilai_api.routers.endpoints.responses.handle_responses_tool_workflow" ) as mock_handle_tool_workflow, @@ -618,9 +612,6 @@ async def test_responses_without_prompt_document(self): mock_model_endpoint.metadata.multimodal_support = True mock_get_model.return_value = mock_model_endpoint - mock_update_usage.return_value = None - mock_log_query.return_value = None - mock_client_instance = MagicMock() mock_response = MagicMock() mock_response.model_dump.return_value = response_payload @@ -634,8 +625,13 @@ async def test_responses_without_prompt_document(self): mock_meter = MagicMock() mock_meter.set_response = MagicMock() + mock_log_ctx = MagicMock() + await create_response( - req=request, auth_info=mock_auth_info, meter=mock_meter + req=request, + auth_info=mock_auth_info, + meter=mock_meter, + log_ctx=mock_log_ctx, ) mock_get_prompt.assert_not_called() @@ -658,12 +654,10 @@ def test_prompt_delegation_token_model_validation(self): assert token.token == "delegation_token_123" assert token.did == "did:nil:builder123" - def test_user_is_subscription_owner_property( - self, mock_subscription_owner_user, mock_regular_user - ): - """Test the is_subscription_owner property""" - # Subscription owner (userid == apikey) - assert mock_subscription_owner_user.is_subscription_owner is True - - # Regular user (userid != apikey) - assert mock_regular_user.is_subscription_owner is False + def test_user_data_structure(self, mock_subscription_owner_user, mock_regular_user): + """Test the UserData structure has required fields""" + # Check that UserData has the expected fields + assert hasattr(mock_subscription_owner_user, "user_id") + assert hasattr(mock_subscription_owner_user, "rate_limits") + assert hasattr(mock_regular_user, "user_id") + assert hasattr(mock_regular_user, "rate_limits") diff --git a/tests/unit/nilai_api/routers/test_responses_private.py b/tests/unit/nilai_api/routers/test_responses_private.py index cf122c79..d5962dfc 100644 --- a/tests/unit/nilai_api/routers/test_responses_private.py +++ b/tests/unit/nilai_api/routers/test_responses_private.py @@ -24,7 +24,7 @@ async def test_runs_in_a_loop(): @pytest.fixture def mock_user(): mock = MagicMock(spec=UserModel) - mock.userid = "test-user-id" + mock.user_id = "test-user-id" mock.name = "Test User" mock.apikey = "test-api-key" mock.prompt_tokens = 100 @@ -43,61 +43,26 @@ def mock_user_manager(mock_user, mocker): from nilai_api.db.users import UserManager from nilai_api.db.logs import QueryLogManager + # Patch QueryLogManager for usage mocker.patch.object( - UserManager, - "get_token_usage", - return_value={ - "prompt_tokens": 100, - "completion_tokens": 50, - "total_tokens": 150, - "queries": 10, - }, - ) - mocker.patch.object(UserManager, "update_token_usage") - mocker.patch.object( - UserManager, + QueryLogManager, "get_user_token_usage", return_value={ "prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150, - "completion_tokens_details": None, - "prompt_tokens_details": None, "queries": 10, }, ) - mocker.patch.object( - UserManager, - "insert_user", - return_value={ - "userid": "test-user-id", - "apikey": "test-api-key", - "rate_limits": RateLimits().get_effective_limits().model_dump_json(), - }, - ) - mocker.patch.object( - UserManager, - "check_api_key", + mocker.patch.object(QueryLogManager, "log_query") + + # Mock validate_credential for authentication + mocker.patch( + "nilai_api.auth.strategies.validate_credential", + new_callable=AsyncMock, return_value=mock_user, ) - mocker.patch.object( - UserManager, - "get_all_users", - return_value=[ - { - "userid": "test-user-id", - "apikey": "test-api-key", - "rate_limits": RateLimits().get_effective_limits().model_dump_json(), - }, - { - "userid": "test-user-id-2", - "apikey": "test-api-key", - "rate_limits": RateLimits().get_effective_limits().model_dump_json(), - }, - ], - ) - mocker.patch.object(QueryLogManager, "log_query") - mocker.patch.object(UserManager, "update_last_activity") + return UserManager @@ -107,6 +72,7 @@ def mock_state(mocker): mock_discovery_service = mocker.Mock() mock_discovery_service.discover_models = AsyncMock(return_value=expected_models) + mock_discovery_service.initialize = AsyncMock() mocker.patch.object(state, "discovery_service", mock_discovery_service) @@ -138,7 +104,7 @@ def mock_metering_context(mocker): @pytest.fixture -def client(mock_user_manager, mock_metering_context): +def client(mock_user_manager, mock_state, mock_metering_context): from nilai_api.app import app from nilai_api.credit import LLMMeter @@ -210,6 +176,11 @@ def test_create_response(mock_user, mock_state, mock_user_manager, mocker, clien "nilai_api.routers.endpoints.responses.handle_responses_tool_workflow", return_value=(response_data, 0, 0), ) + mocker.patch( + "nilai_api.routers.endpoints.responses.state.get_model", + return_value=model_endpoint, + ) + mocker.patch("nilai_api.db.logs.QueryLogContext.commit", new_callable=AsyncMock) payload = { "model": "meta-llama/Llama-3.2-1B-Instruct", @@ -308,6 +279,11 @@ async def chunk_generator(): "nilai_api.routers.endpoints.responses.AsyncOpenAI", return_value=mock_async_openai_instance, ) + mocker.patch( + "nilai_api.routers.endpoints.responses.state.get_model", + return_value=model_endpoint, + ) + mocker.patch("nilai_api.db.logs.QueryLogContext.commit", new_callable=AsyncMock) payload = { "model": "meta-llama/Llama-3.2-1B-Instruct", diff --git a/tests/unit/nilai_api/test_db.py b/tests/unit/nilai_api/test_db.py index dff0fd8b..3979321d 100644 --- a/tests/unit/nilai_api/test_db.py +++ b/tests/unit/nilai_api/test_db.py @@ -15,7 +15,7 @@ async def test_insert_user(mock_db): """Test user insertion functionality.""" user = await mock_db.insert_user("Test User", "test@example.com") - assert "userid" in user + assert "user_id" in user assert "apikey" in user assert len(mock_db.users) == 1 @@ -38,9 +38,9 @@ async def test_token_usage(mock_db): """Test token usage tracking.""" user = await mock_db.insert_user("Test User", "test@example.com") - await mock_db.update_token_usage(user["userid"], 50, 20) + await mock_db.update_token_usage(user["user_id"], 50, 20) - token_usage = await mock_db.get_token_usage(user["userid"]) + token_usage = await mock_db.get_token_usage(user["user_id"]) assert token_usage["prompt_tokens"] == 50 assert token_usage["completion_tokens"] == 20 assert token_usage["queries"] == 1 @@ -51,9 +51,9 @@ async def test_query_logging(mock_db): """Test query logging functionality.""" user = await mock_db.insert_user("Test User", "test@example.com") - await mock_db.log_query(user["userid"], "test-model", 10, 15) + await mock_db.log_query(user["user_id"], "test-model", 10, 15) assert len(mock_db.query_logs) == 1 log_entry = list(mock_db.query_logs.values())[0] - assert log_entry["userid"] == user["userid"] + assert log_entry["user_id"] == user["user_id"] assert log_entry["model"] == "test-model" diff --git a/tests/unit/nilai_api/test_rate_limiting.py b/tests/unit/nilai_api/test_rate_limiting.py index 27a5c1bc..bd8a41e4 100644 --- a/tests/unit/nilai_api/test_rate_limiting.py +++ b/tests/unit/nilai_api/test_rate_limiting.py @@ -45,7 +45,7 @@ async def test_concurrent_rate_limit(req): rate_limit = RateLimit(concurrent_extractor=lambda _: (5, "test")) user_limits = UserRateLimits( - subscription_holder=random_id(), + user_id=random_id(), token_rate_limit=None, rate_limits=RateLimits( user_rate_limit_day=None, @@ -86,7 +86,7 @@ async def web_search_extractor(_): rate_limit = RateLimit(web_search_extractor=web_search_extractor) user_limits = UserRateLimits( - subscription_holder=random_id(), + user_id=random_id(), token_rate_limit=None, rate_limits=RateLimits( user_rate_limit_day=None, @@ -117,7 +117,7 @@ async def web_search_extractor(_): "user_limits", [ UserRateLimits( - subscription_holder=random_id(), + user_id=random_id(), token_rate_limit=None, rate_limits=RateLimits( user_rate_limit_day=10, @@ -131,7 +131,7 @@ async def web_search_extractor(_): ), ), UserRateLimits( - subscription_holder=random_id(), + user_id=random_id(), token_rate_limit=None, rate_limits=RateLimits( user_rate_limit_day=None, @@ -145,7 +145,7 @@ async def web_search_extractor(_): ), ), UserRateLimits( - subscription_holder=random_id(), + user_id=random_id(), token_rate_limit=None, rate_limits=RateLimits( user_rate_limit_day=None, @@ -159,7 +159,7 @@ async def web_search_extractor(_): ), ), UserRateLimits( - subscription_holder=random_id(), + user_id=random_id(), token_rate_limit=TokenRateLimits( limits=[ TokenRateLimit( @@ -220,7 +220,7 @@ async def web_search_extractor(request): rate_limit = RateLimit(web_search_extractor=web_search_extractor) user_limits = UserRateLimits( - subscription_holder=apikey, + user_id=apikey, token_rate_limit=None, rate_limits=RateLimits( user_rate_limit_day=None, diff --git a/uv.lock b/uv.lock index 86f82f95..03918e23 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,25 @@ members = [ "nilai-api", "nilai-common", "nilai-models", + "nilai-py", +] + +[[package]] +name = "accelerate" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, ] [[package]] @@ -168,6 +187,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] +[[package]] +name = "asn1crypto" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" }, +] + [[package]] name = "asyncpg" version = "0.30.0" @@ -201,6 +229,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "authlib" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", version = "45.0.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'" }, + { name = "cryptography", version = "46.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" }, +] + [[package]] name = "babel" version = "2.17.0" @@ -572,6 +613,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/ca/6a667ccbe649856dcd3458bab80b016681b274399d6211187c6ab969fc50/courlan-1.3.2-py3-none-any.whl", hash = "sha256:d0dab52cf5b5b1000ee2839fbc2837e93b2514d3cb5bb61ae158a55b7a04c6be", size = 33848, upload-time = "2024-10-29T16:40:18.325Z" }, ] +[[package]] +name = "coverage" +version = "7.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, + { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, + { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, + { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, + { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, + { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, + { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, + { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, + { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, + { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, + { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, + { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, + { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, + { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, + { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, + { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, +] + [[package]] name = "cryptography" version = "45.0.7" @@ -802,18 +917,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453, upload-time = "2025-06-26T09:29:21.412Z" }, ] -[[package]] -name = "debtcollector" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/e2/a45b5a620145937529c840df5e499c267997e85de40df27d54424a158d3c/debtcollector-3.0.0.tar.gz", hash = "sha256:2a8917d25b0e1f1d0d365d3c1c6ecfc7a522b1e9716e8a1a4a915126f7ccea6f", size = 31322, upload-time = "2024-02-22T15:39:20.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/ca/863ed8fa66d6f986de6ad7feccc5df96e37400845b1eeb29889a70feea99/debtcollector-3.0.0-py3-none-any.whl", hash = "sha256:46f9dacbe8ce49c47ebf2bf2ec878d50c9443dfae97cc7b8054be684e54c3e91", size = 23035, upload-time = "2024-02-22T15:39:18.99Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -933,20 +1036,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] -[[package]] -name = "etcd3gw" -version = "2.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "futurist" }, - { name = "pbr" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/56/db0e19678af91d9213cf21c72e7d82a3494d6fc7da16d61c6ba578fd8648/etcd3gw-2.4.2.tar.gz", hash = "sha256:6c6e9e42b810ee9a9455dd342de989f1fab637a94daa4fc34cacb248a54473fa", size = 29840, upload-time = "2024-08-27T16:21:35.772Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/11/79f09e0d1195d455bdf0542d4fec4ddc80a4f496d090244bba9fc7113834/etcd3gw-2.4.2-py3-none-any.whl", hash = "sha256:b907bd2dc702eabbeba3f9c15666e94e92961bfe685429a0e415ce44097f5c22", size = 24092, upload-time = "2024-08-27T16:21:34.556Z" }, -] - [[package]] name = "eth-abi" version = "5.2.0" @@ -1316,18 +1405,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, ] -[[package]] -name = "futurist" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "debtcollector" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/12/786f4aaf9d396d67b1b7b90f248ff994e916605d0751d08a0344a4a785a6/futurist-3.2.1.tar.gz", hash = "sha256:01dd4f30acdfbb2e2eb6091da565eded82d8cbaf6c48a36cc7f73c11cfa7fb3f", size = 49326, upload-time = "2025-08-29T15:06:57.733Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/5b/a4418215b594fa44dea7deae61fa406139e2e8acc6442d25f93d80c52c84/futurist-3.2.1-py3-none-any.whl", hash = "sha256:c76a1e7b2c6b264666740c3dffbdcf512bd9684b4b253a3068a0135b43729745", size = 40485, upload-time = "2025-08-29T15:06:56.476Z" }, -] - [[package]] name = "googleapis-common-protos" version = "1.72.0" @@ -2071,6 +2148,7 @@ dependencies = [ { name = "nilai-api" }, { name = "nilai-common" }, { name = "nilai-models" }, + { name = "nilai-py" }, ] [package.dev-dependencies] @@ -2093,15 +2171,16 @@ requires-dist = [ { name = "nilai-api", editable = "nilai-api" }, { name = "nilai-common", editable = "packages/nilai-common" }, { name = "nilai-models", editable = "nilai-models" }, + { name = "nilai-py", editable = "clients/nilai-py" }, ] [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=25.9.0" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "isort", specifier = ">=6.1.0" }, + { name = "isort", specifier = ">=7.0.0" }, { name = "pre-commit", specifier = ">=4.1.0" }, - { name = "pyright", specifier = ">=1.1.405" }, + { name = "pyright", specifier = ">=1.1.406" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-mock", specifier = ">=3.14.0" }, @@ -2115,63 +2194,64 @@ name = "nilai-api" version = "0.1.0" source = { editable = "nilai-api" } dependencies = [ + { name = "accelerate" }, { name = "alembic" }, { name = "asyncpg" }, + { name = "authlib" }, { name = "click" }, + { name = "cryptography", version = "45.0.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'" }, + { name = "cryptography", version = "46.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or platform_python_implementation == 'PyPy'" }, { name = "e2b-code-interpreter" }, - { name = "ecdsa" }, - { name = "eth-account" }, { name = "fastapi", extra = ["standard"] }, + { name = "greenlet" }, { name = "gunicorn" }, - { name = "hexbytes" }, { name = "httpx" }, { name = "nilai-common" }, { name = "nilauth-credit-middleware" }, { name = "nilrag" }, { name = "nuc" }, { name = "openai" }, + { name = "pg8000" }, { name = "prometheus-fastapi-instrumentator" }, - { name = "pydantic" }, { name = "python-dotenv" }, { name = "pyyaml" }, { name = "redis" }, - { name = "secp256k1" }, { name = "secretvaults" }, - { name = "sentence-transformers" }, { name = "sqlalchemy" }, { name = "trafilatura" }, { name = "uvicorn" }, + { name = "verifier" }, { name = "web3" }, ] [package.metadata] requires-dist = [ + { name = "accelerate", specifier = ">=1.1.1" }, { name = "alembic", specifier = ">=1.14.1" }, { name = "asyncpg", specifier = ">=0.30.0" }, + { name = "authlib", specifier = ">=1.4.1" }, { name = "click", specifier = ">=8.1.8" }, + { name = "cryptography", specifier = ">=43.0.1" }, { name = "e2b-code-interpreter", specifier = ">=1.0.3" }, - { name = "ecdsa", specifier = ">=0.19.0" }, - { name = "eth-account", specifier = ">=0.13.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.5" }, + { name = "greenlet", specifier = ">=3.1.1" }, { name = "gunicorn", specifier = ">=23.0.0" }, - { name = "hexbytes", specifier = ">=1.2.0" }, { name = "httpx", specifier = ">=0.27.2" }, { name = "nilai-common", editable = "packages/nilai-common" }, - { name = "nilauth-credit-middleware", specifier = "==0.1.1" }, + { name = "nilauth-credit-middleware", specifier = ">=0.1.2" }, { name = "nilrag", specifier = ">=0.1.11" }, { name = "nuc", specifier = ">=0.1.0" }, - { name = "openai", specifier = ">=1.99.2" }, + { name = "openai", specifier = ">=1.59.9" }, + { name = "pg8000", specifier = ">=1.31.2" }, { name = "prometheus-fastapi-instrumentator", specifier = ">=7.0.2" }, - { name = "pydantic", specifier = ">=2.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "pyyaml", specifier = ">=6.0.1" }, - { name = "redis", specifier = ">=6.4.0" }, - { name = "secp256k1", specifier = ">=0.14.0" }, + { name = "redis", specifier = ">=5.2.1" }, { name = "secretvaults", git = "https://github.com/jcabrero/secretvaults-py?rev=main" }, - { name = "sentence-transformers", specifier = ">=5.1.1" }, { name = "sqlalchemy", specifier = ">=2.0.36" }, { name = "trafilatura", specifier = ">=1.7.0" }, { name = "uvicorn", specifier = ">=0.32.1" }, + { name = "verifier" }, { name = "web3", specifier = ">=7.8.0" }, ] @@ -2180,17 +2260,17 @@ name = "nilai-common" version = "0.1.0" source = { editable = "packages/nilai-common" } dependencies = [ - { name = "etcd3gw" }, { name = "openai" }, { name = "pydantic" }, + { name = "redis" }, { name = "tenacity" }, ] [package.metadata] requires-dist = [ - { name = "etcd3gw", specifier = ">=2.4.2" }, { name = "openai", specifier = ">=1.99.2" }, { name = "pydantic", specifier = ">=2.10.1" }, + { name = "redis", specifier = ">=5.0.0" }, { name = "tenacity", specifier = ">=9.0.0" }, ] @@ -2209,9 +2289,46 @@ requires-dist = [ { name = "nilai-common", editable = "packages/nilai-common" }, ] +[[package]] +name = "nilai-py" +version = "0.0.0a0" +source = { editable = "clients/nilai-py" } +dependencies = [ + { name = "httpx" }, + { name = "nuc" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "secretvaults" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "nuc", specifier = ">=0.1.0" }, + { name = "openai", specifier = ">=1.108.1" }, + { name = "pydantic", specifier = ">=2.11.9" }, + { name = "python-dotenv", specifier = ">=1.1.1" }, + { name = "secretvaults", specifier = ">=0.2.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "ruff", specifier = ">=0.13.1" }, +] + [[package]] name = "nilauth-credit-middleware" -version = "0.1.1" +version = "0.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi", extra = ["standard"] }, @@ -2219,9 +2336,9 @@ dependencies = [ { name = "nuc" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/cf/7716fa5f4aca83ef39d6f9f8bebc1d80d194c52c9ce6e75ee6bd1f401217/nilauth_credit_middleware-0.1.1.tar.gz", hash = "sha256:ae32c4c1e6bc083c8a7581d72a6da271ce9c0f0f9271a1694acb81ccd0a4a8bd", size = 10259, upload-time = "2025-10-16T11:15:03.918Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/bc/ae9b2c26919151fc7193b406a98831eeef197f6ec46b0c075138e66ec016/nilauth_credit_middleware-0.1.2.tar.gz", hash = "sha256:66423a4d18aba1eb5f5d47a04c8f7ae6a19ab4e34433475aa9dc1ba398483fdd", size = 11979, upload-time = "2025-10-30T16:21:20.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/b5/6e4090ae2ae8848d12e43f82d8d995cd1dff9de8e947cf5fb2b8a72a828e/nilauth_credit_middleware-0.1.1-py3-none-any.whl", hash = "sha256:10a0fda4ac11f51b9a5dd7b3a8fbabc0b28ff92a170a7729ac11eb15c7b37887", size = 14919, upload-time = "2025-10-16T11:15:02.201Z" }, + { url = "https://files.pythonhosted.org/packages/05/c3/73d55667aad701a64f3d1330d66c90a8c292fd19f054093ca74960aca1fb/nilauth_credit_middleware-0.1.2-py3-none-any.whl", hash = "sha256:31f3233e6706c6167b6246a4edb9a405d587eccb1399231223f95c0cdf1ce57c", size = 18121, upload-time = "2025-10-30T16:21:19.547Z" }, ] [[package]] @@ -2491,15 +2608,16 @@ wheels = [ ] [[package]] -name = "pbr" -version = "7.0.3" +name = "pg8000" +version = "1.31.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "setuptools" }, + { name = "python-dateutil" }, + { name = "scramp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/9a/077ab21e700051e03d8c5232b6bcb9a1a4d4b6242c9a0226df2cfa306414/pg8000-1.31.5.tar.gz", hash = "sha256:46ebb03be52b7a77c03c725c79da2ca281d6e8f59577ca66b17c9009618cae78", size = 118933, upload-time = "2025-09-14T09:16:49.748Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, + { url = "https://files.pythonhosted.org/packages/45/07/5fd183858dff4d24840f07fc845f213cd371a19958558607ba22035dadd7/pg8000-1.31.5-py3-none-any.whl", hash = "sha256:0af2c1926b153307639868d2ee5cef6cd3a7d07448e12736989b10e1d491e201", size = 57816, upload-time = "2025-09-14T09:16:47.798Z" }, ] [[package]] @@ -2725,6 +2843,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757, upload-time = "2025-05-28T14:22:24.135Z" }, ] +[[package]] +name = "psutil" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -2921,6 +3065,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "pytest-mock" version = "3.15.1" @@ -3508,6 +3666,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, ] +[[package]] +name = "scramp" +version = "1.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/77/6db18bab446c12cfbee22ca8f65d5b187966bd8f900aeb65db9e60d4be3d/scramp-1.4.6.tar.gz", hash = "sha256:fe055ebbebf4397b9cb323fcc4b299f219cd1b03fd673ca40c97db04ac7d107e", size = 16306, upload-time = "2025-07-05T14:44:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/bf/54b5d40bea1c1805175ead2d496c267f05eec87561687dd73ab76869d8d9/scramp-1.4.6-py3-none-any.whl", hash = "sha256:a0cf9d2b4624b69bac5432dd69fecfc55a542384fe73c3a23ed9b138cda484e1", size = 12812, upload-time = "2025-07-05T14:44:02.345Z" }, +] + [[package]] name = "secp256k1" version = "0.14.0" @@ -3987,6 +4157,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] +[[package]] +name = "verifier" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/5b/dd27d685666dba902f70893d66f7d280a2bbeab81eed805dd683ee299459/verifier-1.0.0.tar.gz", hash = "sha256:268e0b6c1744d95601421fa93da4be4112b208f182236f2c66350b5c4dfc972f", size = 1585, upload-time = "2019-02-12T13:38:28.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/5e/cf1b2cd9010ba5575cfb376a6f86f6bb2afdd23cfc57aa818bf22b4f61ec/verifier-1.0.0-py3-none-any.whl", hash = "sha256:fd456f5e4b1f1ea3a0ab028e5e75c72bdfc4be5bd8d06490309ae6c383d08fd9", size = 2834, upload-time = "2019-02-12T13:38:26.162Z" }, +] + [[package]] name = "virtualenv" version = "20.35.4"