diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 000000000..a5a97658e --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,5 @@ +version = 1 +name = "langfuse-python" + +[setup] +script = "bash scripts/codex/setup.sh" diff --git a/.env.template b/.env.template index e68327220..77541addf 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,5 @@ # Langfuse -LANGFUSE_HOST=http://localhost:3000 +LANGFUSE_BASE_URL=http://localhost:3000 LANGFUSE_PUBLIC_KEY=pk-lf-1234567890 LANGFUSE_SECRET_KEY=sk-lf-1234567890 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b3e7ece6e..c7993b0b4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,5 @@ # Currently inactive # * @langfuse/maintainers + +# Require maintainer review for GitHub configuration changes +.github/ @langfuse/maintainers diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..a06986558 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +## What does this PR do? + +> PR title must follow Conventional Commits, for example `feat: add dataset scoring helper` or `fix(openai): preserve trace context`. + +Fixes # + +## Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Refactor +- [ ] Documentation update +- [ ] Tooling, CI, or repo maintenance + +## Verification + +List the main commands you ran: + +```bash + +``` + +## Checklist + +- [ ] I self-reviewed the diff using `code_review.md`. +- [ ] I added or updated tests for behavior changes. +- [ ] I updated docs, examples, or `.env.template` if needed. +- [ ] I did not hand-edit generated files; if generated files changed, I used the upstream regeneration path. +- [ ] I did not commit secrets or credentials. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 04b490b64..e312aaf57 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,10 +5,12 @@ version: 2 updates: - - package-ecosystem: "pip" # See documentation for possible values + - package-ecosystem: "uv" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" + cooldown: + default-days: 7 rebase-strategy: "disabled" # use dependabot-rebase-stale commit-message: prefix: chore @@ -21,3 +23,19 @@ updates: llama-index: patterns: - "llama-index*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + rebase-strategy: "disabled" + commit-message: + prefix: chore + prefix-development: chore + include: scope + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e1ae37f7..942df09b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,39 +10,43 @@ on: branches: - "*" +permissions: {} + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: false + cancel-in-progress: true jobs: linting: - runs-on: ubuntu-latest + runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - - uses: actions/checkout@v3 - - uses: chartboost/ruff-action@v1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - args: check --config ci.ruff.toml + persist-credentials: false + - name: Install uv and set Python version + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.2" + python-version: "3.13" + enable-cache: true # zizmor: ignore[cache-poisoning] CI-only, no artifacts published + - name: Install dependencies + run: uv sync --locked + - name: Run Ruff + run: uv run --frozen ruff check . type-checking: - runs-on: ubuntu-latest + runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - python-version: "3.12" - - name: Install poetry - uses: abatilo/actions-poetry@v2 - - name: Setup a local virtual environment - run: | - poetry config virtualenvs.create true --local - poetry config virtualenvs.in-project true --local - - uses: actions/cache@v3 - name: Define a cache for the virtual environment based on the dependencies lock file + persist-credentials: false + - name: Install uv and set Python version + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: - path: ./.venv - key: venv-type-check-${{ hashFiles('poetry.lock') }} - - uses: actions/cache@v3 + version: "0.11.2" + python-version: "3.13" + enable-cache: true # zizmor: ignore[cache-poisoning] CI-only, no artifacts published + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # zizmor: ignore[cache-poisoning] name: Cache mypy cache with: path: ./.mypy_cache @@ -50,99 +54,145 @@ jobs: restore-keys: | mypy- - name: Install dependencies - run: poetry install --only=main,dev + run: uv sync --locked - name: Run mypy type checking - run: poetry run mypy langfuse --no-error-summary + run: uv run --frozen mypy langfuse --no-error-summary - ci: - runs-on: ubuntu-latest + unit-tests: + runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 env: - LANGFUSE_HOST: "http://localhost:3000" - LANGFUSE_PUBLIC_KEY: "pk-lf-1234567890" - LANGFUSE_SECRET_KEY: "sk-lf-1234567890" - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # SERPAPI_API_KEY: ${{ secrets.SERPAPI_API_KEY }} - HUGGINGFACEHUB_API_TOKEN: ${{ secrets.HUGGINGFACEHUB_API_TOKEN }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + LANGFUSE_BASE_URL: "http://localhost:3000" + LANGFUSE_PUBLIC_KEY: "pk-lf-test" + LANGFUSE_SECRET_KEY: "sk-lf-test" + OPENAI_API_KEY: "test-openai-key" strategy: fail-fast: false matrix: python-version: - - "3.9" - "3.10" - "3.11" + - "3.12" + - "3.13" + - "3.14" - name: Test on Python version ${{ matrix.python-version }} + name: Unit tests on Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v3 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Install uv and set Python version + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: - version: 9.5.0 + version: "0.11.2" + python-version: ${{ matrix.python-version }} + enable-cache: true # zizmor: ignore[cache-poisoning] CI-only, no artifacts published - - name: Clone langfuse server + - name: Check Python version + run: python --version + + - name: Install the project dependencies + run: uv sync --locked + + - name: Run the automated tests run: | - git clone https://github.com/langfuse/langfuse.git ./langfuse-server && echo $(cd ./langfuse-server && git rev-parse HEAD) + python --version + uv run --frozen pytest -n auto --dist worksteal -s -v --log-cli-level=INFO tests/unit - - name: Setup node (for langfuse server) - uses: actions/setup-node@v3 - with: - node-version: 20 + e2e-tests: + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - suite: e2e + job_name: E2E shard 1 tests on Python 3.13 + shard_name: shard-1 + shard_index: 0 + shard_count: 2 + - suite: e2e + job_name: E2E shard 2 tests on Python 3.13 + shard_name: shard-2 + shard_index: 1 + shard_count: 2 + - suite: live_provider + job_name: E2E live-provider tests on Python 3.13 + shard_name: live-provider + env: + LANGFUSE_BASE_URL: "http://localhost:3000" + LANGFUSE_PUBLIC_KEY: "pk-lf-1234567890" + LANGFUSE_SECRET_KEY: "sk-lf-1234567890" + LANGFUSE_INIT_ORG_ID: "0c6c96f4-0ca0-4f16-92a8-6dd7d7c6a501" + LANGFUSE_INIT_ORG_NAME: "SDK Test Org" + LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" + LANGFUSE_INIT_PROJECT_NAME: "SDK Test Project" + LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890" + LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-1234567890" + LANGFUSE_INIT_USER_EMAIL: "sdk-tests@langfuse.local" + LANGFUSE_INIT_USER_NAME: "SDK Tests" + LANGFUSE_INIT_USER_PASSWORD: "langfuse-ci-password" + LANGFUSE_E2E_READ_TIMEOUT_SECONDS: "60" + LANGFUSE_E2E_READ_INTERVAL_SECONDS: "0.5" + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # SERPAPI_API_KEY: ${{ secrets.SERPAPI_API_KEY }} + HUGGINGFACEHUB_API_TOKEN: ${{ secrets.HUGGINGFACEHUB_API_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - - name: Cache langfuse server dependencies - uses: actions/cache@v3 + name: ${{ matrix.job_name }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - path: ./langfuse-server/node_modules - key: | - langfuse-server-${{ hashFiles('./langfuse-server/package-lock.json') }} - langfuse-server- + persist-credentials: false + - name: Install uv and set Python version + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.2" + python-version: "3.13" + enable-cache: true # zizmor: ignore[cache-poisoning] CI-only, no artifacts published + - name: Install the project dependencies + run: uv sync --locked + - name: Check uv Python version + run: uv run --frozen python --version + - name: Prepare langfuse server compose + run: | + mkdir -p ./langfuse-server + LANGFUSE_SERVER_SHA="$(git ls-remote https://github.com/langfuse/langfuse.git HEAD | cut -f1)" + curl -fsSL "https://raw.githubusercontent.com/langfuse/langfuse/${LANGFUSE_SERVER_SHA}/docker-compose.yml" \ + -o ./langfuse-server/docker-compose.yml + echo "${LANGFUSE_SERVER_SHA}" - name: Run langfuse server run: | cd ./langfuse-server - echo "::group::Run langfuse server" - TELEMETRY_ENABLED=false docker compose up -d postgres - echo "::endgroup::" - - echo "::group::Logs from langfuse server" - TELEMETRY_ENABLED=false docker compose logs - echo "::endgroup::" - - echo "::group::Install dependencies (necessary to run seeder)" - pnpm i - echo "::endgroup::" - - echo "::group::Seed db" - cp .env.dev.example .env - pnpm run db:migrate - pnpm run db:seed - echo "::endgroup::" - rm -rf .env - - echo "::group::Run server" - + echo "::group::Start langfuse server" TELEMETRY_ENABLED=false \ + NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=true \ LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9090 \ LANGFUSE_INGESTION_QUEUE_DELAY_MS=10 \ LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=10 \ + LANGFUSE_EXPERIMENT_INSERT_INTO_EVENTS_TABLE=true \ + QUEUE_CONSUMER_EVENT_PROPAGATION_QUEUE_IS_ENABLED=true \ + LANGFUSE_ENABLE_EVENTS_TABLE_V2_APIS=true \ + LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS=true \ docker compose up -d - echo "::endgroup::" - # Add this step to check the health of the container - name: Health check for langfuse server run: | echo "Checking if the langfuse server is up..." retry_count=0 - max_retries=10 - until curl --output /dev/null --silent --head --fail http://localhost:3000/api/public/health + max_retries=20 + until curl --output /dev/null --silent --head --fail http://localhost:3000/api/public/health && \ + uv run --frozen python -c "from langfuse import Langfuse; client = Langfuse(); project_id = client._get_project_id(); assert project_id == '7a88fb47-b4e2-43b8-a06c-a5ce950dc53a', project_id; print(project_id)" do retry_count=`expr $retry_count + 1` echo "Attempt $retry_count of $max_retries..." if [ $retry_count -ge $max_retries ]; then echo "Langfuse server did not respond in time. Printing logs..." - docker logs langfuse-server-langfuse-web-1 + (cd ./langfuse-server && docker compose ps) + (cd ./langfuse-server && docker compose logs langfuse-web langfuse-worker) echo "Failing the step..." exit 1 fi @@ -150,48 +200,58 @@ jobs: done echo "Langfuse server is up and running!" - - name: Install Python - uses: actions/setup-python@v4 - # see details (matrix, python-version, python-version-file, etc.) - # https://github.com/actions/setup-python - with: - python-version: ${{ matrix.python-version }} - - - name: Check python version - run: python --version - - - name: Install poetry - uses: abatilo/actions-poetry@v2 - - - name: Set poetry python version + - name: Select e2e shard files + if: ${{ matrix.suite == 'e2e' }} run: | - poetry env use ${{ matrix.python-version }} - poetry env info + uv run --frozen python scripts/select_e2e_shard.py \ + --shard-index ${{ matrix.shard_index }} \ + --shard-count ${{ matrix.shard_count }} \ + --json + uv run --frozen python scripts/select_e2e_shard.py \ + --shard-index ${{ matrix.shard_index }} \ + --shard-count ${{ matrix.shard_count }} \ + > "$RUNNER_TEMP/e2e-shard-files.txt" + cat "$RUNNER_TEMP/e2e-shard-files.txt" - - name: Setup a local virtual environment (if no poetry.toml file) + - name: Run the parallel end-to-end tests + if: ${{ matrix.suite == 'e2e' }} run: | - poetry config virtualenvs.create true --local - poetry config virtualenvs.in-project true --local + uv run --frozen python --version + mapfile -t e2e_files < "$RUNNER_TEMP/e2e-shard-files.txt" + set +e + uv run --frozen pytest -n 4 --dist worksteal -s -v --log-cli-level=INFO "${e2e_files[@]}" -m "not serial_e2e" + status=$? + set -e + if [ "$status" -eq 5 ]; then + echo "No parallel e2e tests selected for this shard." + elif [ "$status" -ne 0 ]; then + exit "$status" + fi - - uses: actions/cache@v3 - name: Define a cache for the virtual environment based on the dependencies lock file - with: - path: ./.venv - key: | - venv-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}-${{ github.sha }} - - - name: Install the project dependencies - run: poetry install --all-extras + - name: Run serial end-to-end tests + if: ${{ matrix.suite == 'e2e' }} + run: | + mapfile -t e2e_files < "$RUNNER_TEMP/e2e-shard-files.txt" + set +e + uv run --frozen pytest -s -v --log-cli-level=INFO "${e2e_files[@]}" -m "serial_e2e" + status=$? + set -e + if [ "$status" -eq 5 ]; then + echo "No serial e2e tests selected for this shard." + elif [ "$status" -ne 0 ]; then + exit "$status" + fi - - name: Run the automated tests + - name: Run live-provider tests + if: ${{ matrix.suite == 'live_provider' }} run: | - python --version - poetry run pytest -n auto --dist loadfile -s -v --log-cli-level=INFO + uv run --frozen python --version + uv run --frozen pytest -n 4 --dist worksteal -s -v --log-cli-level=INFO tests/live_provider -m "live_provider" all-tests-passed: # This allows us to have a branch protection rule for tests and deploys with matrix - runs-on: ubuntu-latest - needs: [ci, linting, type-checking] + runs-on: blacksmith-2vcpu-ubuntu-2404 + needs: [unit-tests, e2e-tests, linting, type-checking] if: always() steps: - name: Successful deploy diff --git a/.github/workflows/claude-review-maintainer-prs.yml b/.github/workflows/claude-review-maintainer-prs.yml new file mode 100644 index 000000000..3aa865bdd --- /dev/null +++ b/.github/workflows/claude-review-maintainer-prs.yml @@ -0,0 +1,69 @@ +name: Claude Review on Maintainer PRs + +on: + pull_request: + types: + - opened + - ready_for_review + +jobs: + comment: + # Only run on PRs that are not drafts and are from the same repository (i.e., not from forks) + if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Check author permission and existing review request + id: check + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.pull_request.number; + const username = context.payload.pull_request.user.login; + + let permission = "none"; + try { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, + repo, + username, + }); + permission = data.permission; + } catch (error) { + if (error.status !== 404) { + throw error; + } + } + + const canWrite = ["write", "admin"].includes(permission); + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + const hasReviewRequest = comments.some( + (comment) => comment.body?.trim() === "@claude review", + ); + + core.info( + `PR #${issue_number} by ${username}: permission=${permission}, hasReviewRequest=${hasReviewRequest}`, + ); + + core.setOutput("should_comment", canWrite && !hasReviewRequest ? "true" : "false"); + + - name: Add Claude review comment + if: steps.check.outputs.should_comment == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: "@claude review", + }); diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 92cc5c1fc..0d0b1938d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -55,11 +55,13 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -87,6 +89,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependabot-merge.yml b/.github/workflows/dependabot-merge.yml index 043b7198d..8e5040e77 100644 --- a/.github/workflows/dependabot-merge.yml +++ b/.github/workflows/dependabot-merge.yml @@ -11,11 +11,11 @@ permissions: jobs: dependabot: runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' }} + if: github.event.pull_request.user.id == 49699333 # dependabot[bot] steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1 + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs diff --git a/.github/workflows/dependabot-rebase-stale.yml b/.github/workflows/dependabot-rebase-stale.yml index 79d85964c..534506864 100644 --- a/.github/workflows/dependabot-rebase-stale.yml +++ b/.github/workflows/dependabot-rebase-stale.yml @@ -6,12 +6,14 @@ on: - main workflow_dispatch: +permissions: {} + jobs: rebase-dependabot: runs-on: ubuntu-latest steps: - name: "Rebase open Dependabot PR" - uses: orange-buffalo/dependabot-auto-rebase@v1 + uses: orange-buffalo/dependabot-auto-rebase@fa9e05d7a8152381af0a92ffca942a0d46712544 # v1 with: api-token: ${{ secrets.DEP_REBASE_PAT }} repository: ${{ github.repository }} diff --git a/.github/workflows/package-availability-check.yml b/.github/workflows/package-availability-check.yml index 70c53942b..836213f89 100644 --- a/.github/workflows/package-availability-check.yml +++ b/.github/workflows/package-availability-check.yml @@ -5,6 +5,8 @@ on: - cron: "*/30 * * * *" workflow_dispatch: +permissions: {} + jobs: build: runs-on: ubuntu-latest @@ -15,7 +17,7 @@ jobs: steps: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies using pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0aa2f8d52..96848192f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,19 +1,522 @@ +name: Release Python SDK + on: workflow_dispatch: - push: - # Pattern matched against refs/tags - tags: - - "v[0-9]+.[0-9]+.[0-9]+" # Semantic version tags + inputs: + version: + description: "Version bump type" + required: true + type: choice + options: + - patch + - minor + - major + - prepatch + - preminor + - premajor + prerelease_type: + description: 'Pre-release type (used when version is prepatch/preminor/premajor)' + type: choice + default: "" + options: + - "" + - alpha + - beta + - rc + prerelease_increment: + description: "Pre-release number (e.g., 1 for alpha1). Leave empty to auto-increment or start at 1." + type: string + default: "" + confirm_major: + description: "Type RELEASE MAJOR to confirm a major or premajor release" + required: false + default: "" + +permissions: + contents: write + id-token: write # Required for PyPI Trusted Publishing via OIDC + +concurrency: + group: python-sdk-release + cancel-in-progress: false jobs: - release: + release-python-sdk: runs-on: ubuntu-latest - environment: "protected branches" + environment: protected branches steps: - - uses: actions/checkout@v4 + - name: Verify release ref + run: | + if [ "${GITHUB_REF_TYPE}" != "branch" ]; then + echo "❌ Error: Releases can only be triggered from branches" + echo "Current ref: ${GITHUB_REF}" + exit 1 + fi + + if [ "${GITHUB_REF_NAME}" = "main" ]; then + echo "✅ Official and pre-release releases are allowed from main" + exit 0 + fi + + case "${INPUTS_VERSION}" in + prepatch|preminor|premajor) + ;; + *) + echo "❌ Error: Official releases can only be triggered from main branch" + echo "Current branch: ${GITHUB_REF_NAME}" + echo "Requested version bump: ${INPUTS_VERSION}" + exit 1 + ;; + esac + + if [ -z "${INPUTS_PRERELEASE_TYPE}" ]; then + echo "❌ Error: Branch releases must specify prerelease_type as alpha, beta, or rc" + exit 1 + fi + + echo "✅ Pre-release branch release allowed from ${GITHUB_REF_NAME}" + env: + INPUTS_VERSION: ${{ inputs.version }} + INPUTS_PRERELEASE_TYPE: ${{ inputs.prerelease_type }} + + - name: Confirm major release + if: ${{ inputs.version == 'major' || inputs.version == 'premajor' }} + run: | + if [ "${INPUTS_CONFIRM_MAJOR}" != "RELEASE MAJOR" ]; then + echo "❌ For major/premajor releases, set confirm_major to RELEASE MAJOR" + exit 1 + fi + env: + INPUTS_CONFIRM_MAJOR: ${{ inputs.confirm_major }} + + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - ref: main # Always checkout main even for tagged releases fetch-depth: 0 token: ${{ secrets.GH_ACCESS_TOKEN }} - - name: Push to production - run: git push origin +main:production + persist-credentials: false + + - name: Install uv and set Python version + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.2" + python-version: "3.12" + enable-cache: false + + - name: Configure Git + env: + GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + run: | + git config user.name "langfuse-bot" + git config user.email "langfuse-bot@langfuse.com" + + echo "$GH_ACCESS_TOKEN" | gh auth login --with-token + gh auth setup-git + + - name: Get current version + id: current-version + run: | + current_version=$(uv version --short) + echo "version=$current_version" >> $GITHUB_OUTPUT + echo "Current version: $current_version" + + - name: Calculate new version + id: new-version + run: | + current_version="${STEPS_CURRENT_VERSION_OUTPUTS_VERSION}" + version_type="${INPUTS_VERSION}" + prerelease_type="${INPUTS_PRERELEASE_TYPE}" + prerelease_increment="${INPUTS_PRERELEASE_INCREMENT}" + + # Extract base version (strip any pre-release suffix like a1, b2, rc1) + base_version=$(echo "$current_version" | sed -E 's/(a|b|rc)[0-9]+$//') + + # Parse version components + IFS='.' read -r major minor patch <<< "$base_version" + + if echo "$current_version" | grep -qE '(a|b|rc)[0-9]+$'; then + current_is_prerelease="true" + else + current_is_prerelease="false" + fi + + if [ "$version_type" = "premajor" ] || [ "$version_type" = "preminor" ] || [ "$version_type" = "prepatch" ]; then + if [ -z "$prerelease_type" ]; then + echo "❌ Error: prerelease_type must be specified when version is a prerelease variant" + exit 1 + fi + + # Determine the prerelease base version target + case "$version_type" in + premajor) + if [ "$current_is_prerelease" = "true" ] && [ "$minor" -eq 0 ] && [ "$patch" -eq 0 ]; then + target_major=$major + else + target_major=$((major + 1)) + fi + target_minor=0 + target_patch=0 + ;; + preminor) + target_major=$major + if [ "$current_is_prerelease" = "true" ] && [ "$patch" -eq 0 ] && [ "$minor" -gt 0 ]; then + target_minor=$minor + else + target_minor=$((minor + 1)) + fi + target_patch=0 + ;; + prepatch) + target_major=$major + target_minor=$minor + if [ "$current_is_prerelease" = "true" ] && [ "$patch" -gt 0 ]; then + target_patch=$patch + else + target_patch=$((patch + 1)) + fi + ;; + esac + target_base_version="${target_major}.${target_minor}.${target_patch}" + + # Map prerelease type to Python suffix + case "$prerelease_type" in + alpha) suffix="a" ;; + beta) suffix="b" ;; + rc) suffix="rc" ;; + esac + + # Determine prerelease number + if [ -n "$prerelease_increment" ]; then + pre_num="$prerelease_increment" + else + # Check if current version is same type of prerelease, if so increment + escaped_target_base_version=$(echo "$target_base_version" | sed 's/\./\\./g') + if echo "$current_version" | grep -qE "^${escaped_target_base_version}${suffix}[0-9]+$"; then + current_pre_num=$(echo "$current_version" | sed -E "s/^${escaped_target_base_version}${suffix}([0-9]+)$/\1/") + pre_num=$((current_pre_num + 1)) + else + pre_num=1 + fi + fi + + new_version="${target_base_version}${suffix}${pre_num}" + is_prerelease="true" + else + # Standard version bump + case "$version_type" in + patch) + patch=$((patch + 1)) + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + esac + new_version="${major}.${minor}.${patch}" + is_prerelease="false" + fi + + echo "version=$new_version" >> $GITHUB_OUTPUT + echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT + echo "New version: $new_version (prerelease: $is_prerelease)" + env: + STEPS_CURRENT_VERSION_OUTPUTS_VERSION: ${{ steps.current-version.outputs.version }} + INPUTS_VERSION: ${{ inputs.version }} + INPUTS_PRERELEASE_TYPE: ${{ inputs.prerelease_type }} + INPUTS_PRERELEASE_INCREMENT: ${{ inputs.prerelease_increment }} + + - name: Check if tag already exists + run: | + if git rev-parse "v${STEPS_NEW_VERSION_OUTPUTS_VERSION}" >/dev/null 2>&1; then + echo "❌ Error: Tag v${STEPS_NEW_VERSION_OUTPUTS_VERSION} already exists" + exit 1 + fi + echo "✅ Tag v${STEPS_NEW_VERSION_OUTPUTS_VERSION} does not exist" + env: + STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }} + + - name: Update version in pyproject.toml + run: | + uv version ${STEPS_NEW_VERSION_OUTPUTS_VERSION} + env: + STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }} + + - name: Verify version consistency + run: | + pyproject_version=$(uv version --short) + + echo "pyproject.toml version: $pyproject_version" + + if [ "$pyproject_version" != "${STEPS_NEW_VERSION_OUTPUTS_VERSION}" ]; then + echo "❌ Error: Version in files doesn't match expected version" + exit 1 + fi + + echo "✅ Versions are consistent: $pyproject_version" + env: + STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }} + + - name: Build package + run: uv build --no-sources + + - name: Verify build artifacts + run: | + echo "Verifying build artifacts..." + + if [ ! -d "dist" ]; then + echo "❌ Error: dist directory not found" + exit 1 + fi + + wheel_count=$(ls dist/*.whl 2>/dev/null | wc -l) + sdist_count=$(ls dist/*.tar.gz 2>/dev/null | wc -l) + + if [ "$wheel_count" -eq 0 ]; then + echo "❌ Error: No wheel file found in dist/" + exit 1 + fi + + if [ "$sdist_count" -eq 0 ]; then + echo "❌ Error: No source distribution found in dist/" + exit 1 + fi + + echo "✅ Build artifacts:" + ls -lh dist/ + + # Verify the version in the built artifacts matches + expected_version="${STEPS_NEW_VERSION_OUTPUTS_VERSION}" + wheel_file=$(ls dist/*.whl | head -1) + if ! echo "$wheel_file" | grep -q "$expected_version"; then + echo "❌ Error: Wheel filename doesn't contain expected version $expected_version" + echo "Wheel file: $wheel_file" + exit 1 + fi + echo "✅ Artifact version verified" + env: + STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }} + + - name: Smoke test wheel + run: | + uv run --isolated --no-project --with dist/*.whl python -c "from importlib.metadata import version; import langfuse; assert langfuse.__version__ == version('langfuse')" + + - name: Smoke test source distribution + run: | + uv run --isolated --no-project --with dist/*.tar.gz python -c "from importlib.metadata import version; import langfuse; assert langfuse.__version__ == version('langfuse')" + + - name: Commit version changes + run: | + git add pyproject.toml uv.lock + git commit -m "chore: release v${STEPS_NEW_VERSION_OUTPUTS_VERSION}" + env: + STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }} + + - name: Create and push tag + id: push-tag + run: | + git tag "v${STEPS_NEW_VERSION_OUTPUTS_VERSION}" + git push origin "HEAD:${GITHUB_REF_NAME}" + git push origin "v${STEPS_NEW_VERSION_OUTPUTS_VERSION}" + env: + STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }} + + - name: Publish to PyPI + id: publish-pypi + run: uv publish --trusted-publishing always + + - name: Create GitHub Release + run: | + prerelease_flag="" + if [ "${IS_PRERELEASE}" = "true" ]; then + prerelease_flag="--prerelease" + fi + gh release create "v${VERSION}" \ + --title "v${VERSION}" \ + --generate-notes \ + $prerelease_flag \ + dist/*.whl dist/*.tar.gz + env: + GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + VERSION: ${{ steps.new-version.outputs.version }} + IS_PRERELEASE: ${{ steps.new-version.outputs.is_prerelease }} + + - name: Notify Slack on success + if: success() + uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_RELEASES }} + webhook-type: incoming-webhook + payload: | + { + "text": "✅ Langfuse Python SDK v${{ steps.new-version.outputs.version }} published to PyPI", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "✅ Langfuse Python SDK Released", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Version:*\n`v${{ steps.new-version.outputs.version }}`" + }, + { + "type": "mrkdwn", + "text": "*Type:*\n`${{ inputs.version }}${{ inputs.prerelease_type && format(' ({0})', inputs.prerelease_type) || '' }}`" + }, + { + "type": "mrkdwn", + "text": "*Released by:*\n${{ github.actor }}" + }, + { + "type": "mrkdwn", + "text": "*Package:*\n`langfuse`" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "📋 View Release Notes", + "emoji": true + }, + "url": "${{ github.server_url }}/${{ github.repository }}/releases/tag/v${{ steps.new-version.outputs.version }}", + "style": "primary" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "📦 View on PyPI", + "emoji": true + }, + "url": "https://pypi.org/project/langfuse/${{ steps.new-version.outputs.version }}/" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "🔧 View Workflow", + "emoji": true + }, + "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "🔒 Published with Trusted Publishing (OIDC) • 🤖 Automated release" + } + ] + } + ] + } + + - name: Notify Slack on failure + if: failure() + uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_ENGINEERING }} + webhook-type: incoming-webhook + payload: | + { + "text": "❌ Langfuse Python SDK release workflow failed", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "❌ Langfuse Python SDK Release Failed", + "emoji": true + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "⚠️ The release workflow encountered an error and did not complete successfully." + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Requested Version:*\n`${{ inputs.version }}`" + }, + { + "type": "mrkdwn", + "text": "*Pre-release Type:*\n${{ inputs.prerelease_type || 'N/A' }}" + }, + { + "type": "mrkdwn", + "text": "*Triggered by:*\n${{ github.actor }}" + }, + { + "type": "mrkdwn", + "text": "*Current Version:*\n`${{ steps.current-version.outputs.version }}`" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*🔍 Troubleshooting:*\n• Check workflow logs for error details\n• Verify PyPI Trusted Publishing is configured correctly\n• Ensure the version doesn't already exist on PyPI\n• Check if the git tag already exists\n• If partially published, check PyPI and GitHub releases" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "🔧 View Workflow Logs", + "emoji": true + }, + "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "style": "danger" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "📖 PyPI Trusted Publishing Docs", + "emoji": true + }, + "url": "https://docs.pypi.org/trusted-publishers/" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "🚨 Action required • Check workflow logs for details" + } + ] + } + ] + } diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml new file mode 100644 index 000000000..1d94b4f6b --- /dev/null +++ b/.github/workflows/validate-pr-title.yml @@ -0,0 +1,45 @@ +--- +name: "Validate PR Title" + +on: + pull_request: + branches: + - "**" + types: + - opened + - edited + - synchronize + - reopened + +permissions: {} + +jobs: + validate-pr-title: + runs-on: ubuntu-latest + permissions: + statuses: write + pull-requests: read + steps: + - name: Validate PR title follows conventional commits + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + security + requireScope: false + validateSingleCommit: false + ignoreLabels: | + bot + ignore-semantic-pull-request diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000..4caa8f5dd --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,32 @@ +--- +name: Check GitHub Actions + +on: + workflow_dispatch: + push: + branches: + - "main" + merge_group: + pull_request: + branches: + - "main" + +permissions: {} + +jobs: + zizmor: + name: Check GitHub Actions security + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Run zizmor + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + advanced-security: ${{ github.event_name == 'push' && 'true' || 'false' }} + min-severity: low diff --git a/.gitignore b/.gitignore index bebafdd05..bfd138387 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,8 @@ docs tests/mocks/llama-index-storage *.local.* + +# Codex local runtime state +.codex/log/ +.codex/sessions/ +.codex/tmp/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45f88c352..79eff24cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,35 +1,30 @@ repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.2 + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.11.2 + hooks: + - id: uv-lock + + - repo: local hooks: # Run the linter and fix - - id: ruff + - id: uv-ruff-check + name: ruff-check + entry: uv run --frozen ruff check --fix + language: system types_or: [python, pyi, jupyter] - args: [--fix, --config=ci.ruff.toml] + exclude: ^langfuse/api/ # Run the formatter. - - id: ruff-format + - id: uv-ruff-format + name: ruff-format + entry: uv run --frozen ruff format + language: system types_or: [python, pyi, jupyter] + exclude: ^langfuse/api/ - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 - hooks: - - id: mypy - additional_dependencies: - - types-requests - - types-setuptools - - httpx - - pydantic>=1.10.7 - - backoff>=1.10.0 - - openai>=0.27.8 - - wrapt - - packaging>=23.2 - - opentelemetry-api - - opentelemetry-sdk - - opentelemetry-exporter-otlp - - numpy - - langchain>=0.0.309 - - langchain-core - - langgraph - args: [--no-error-summary] + - id: uv-mypy + name: mypy + entry: uv run --frozen mypy langfuse --no-error-summary + language: system files: ^langfuse/ + pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..a1a6f5846 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,169 @@ +# AGENTS.md + +Shared instructions for coding agents working in this repository. + +Keep this file concise, concrete, and repo-specific. If guidance grows large, split it into referenced docs instead of turning this file into a handbook. + +## Maintenance Contract + +- `AGENTS.md` is a living document. +- Update it in the same PR when repo-wide workflows, architecture, CI contracts, release processes, or durable coding defaults materially change. +- Do not edit this file for one-off task preferences. +- Keep this file as the canonical shared agent guide for this repository. + +## Project Summary + +This repository contains the Langfuse Python SDK. + +- `langfuse/_client/`: core SDK, OpenTelemetry integration, resource management, decorators, datasets +- `langfuse/openai.py`: OpenAI instrumentation +- `langfuse/langchain/`: LangChain integration +- `langfuse/_task_manager/`: background consumers for media and score ingestion +- `langfuse/api/`: generated Fern API client, do not hand-edit +- `tests/unit/`: deterministic local tests, no Langfuse server +- `tests/e2e/`: real Langfuse-server tests +- `tests/live_provider/`: live OpenAI / LangChain provider tests +- `tests/support/`: shared helpers for e2e tests +- `scripts/select_e2e_shard.py`: CI shard selector for `tests/e2e` +- `scripts/codex/`: Codex cloud/worktree bootstrap and shared quick checks + +## Working Style + +- Prefer small, targeted changes that preserve existing behavior. +- Do not weaken assertions just to make tests faster or greener. +- If a test is slow, first optimize setup, teardown, polling, or fixtures. +- Keep repo-shared instructions here. Keep personal or machine-specific notes out of version control. +- Keep tests independent and parallel-safe by default. +- For bug fixes, prefer writing or identifying the failing test first, confirm the failure, then implement the fix. +- For complex or ambiguous tasks, plan first, identify the likely verification path, then implement. +- Before final handoff, review the diff for correctness, regressions, missing tests, and accidental generated-file edits. + +## Setup And Quality Commands + +```bash +uv sync --locked +uv run pre-commit install +uv run --frozen ruff check . +uv run --frozen ruff format . +uv run --frozen mypy langfuse --no-error-summary +bash scripts/codex/quick-check.sh +``` + +## Test Commands + +Use the directory-based test split. + +```bash +# Unit tests +uv run --frozen pytest -n auto --dist worksteal tests/unit + +# All e2e tests that can run concurrently +uv run --frozen pytest -n 4 --dist worksteal tests/e2e -m "not serial_e2e" + +# E2E tests that must run serially +uv run --frozen pytest tests/e2e -m "serial_e2e" + +# Live provider tests +uv run --frozen pytest -n 4 --dist worksteal tests/live_provider -m "live_provider" + +# Single test +uv run --frozen pytest tests/unit/test_resource_manager.py::test_pause_signals_score_consumer_shutdown +``` + +Minimum verification matrix: + +| Change scope | Minimum verification | +| --- | --- | +| Docs or comments only | `uv run --frozen ruff format --check .` if Python files changed | +| Python source only | `uv run --frozen ruff check .` + `uv run --frozen mypy langfuse --no-error-summary` + targeted unit tests | +| Unit-test-only change | targeted `uv run --frozen pytest ...` for the changed tests | +| Shutdown, flushing, worker-thread, or OTEL-heavy change | targeted resource-manager/OTEL tests plus affected integration tests when relevant | +| OpenAI or LangChain instrumentation | targeted unit tests using exporter-local assertions; add e2e/live-provider coverage only when unit tests cannot cover behavior | +| Generated API client or public API contract | upstream Fern/OpenAPI regeneration path plus targeted SDK serialization/deserialization tests | +| CI, sharding, or bootstrap | relevant script test plus CI workflow review against this file's CI contract | + +## Test Topology + +### `tests/unit` + +- Must not require a running Langfuse server. +- Prefer in-memory exporters and local fakes over real network calls. +- If tracing behavior is under test, use the shared in-memory fixtures in `tests/conftest.py`. + +### `tests/e2e` + +- Use for persisted backend behavior that genuinely needs a real Langfuse server. +- Prefer bounded polling helpers in `tests/support/` over raw `sleep()` calls. +- Use `serial_e2e` only for tests that are unsafe under shared-server concurrency. +- New e2e files should be named `tests/e2e/test_*.py`. +- Do not add `e2e_core` / `e2e_data` markers. CI shards `tests/e2e` mechanically with `scripts/select_e2e_shard.py`. + +### `tests/live_provider` + +- This suite uses real provider calls and always runs as one dedicated CI suite. +- Do not split or shard `tests/live_provider` into separate smoke and extended jobs unless the team explicitly changes that policy. +- Keep assertions focused on stable provider-facing behavior rather than brittle observation counts. + +## CI Contract + +The main CI workflow currently runs: +- linting on Python 3.13 +- mypy on Python 3.13 +- `tests/unit` on a Python 3.10-3.14 matrix +- `tests/e2e` in 2 mechanical shards plus a serial subset inside each shard +- `tests/live_provider` as one always-on suite +- PR title validation for Conventional Commits + +If you change the e2e split: + +- update `scripts/select_e2e_shard.py`, not marker routing in `tests/conftest.py` +- make sure new `tests/e2e/test_*.py` files are automatically covered +- keep `serial_e2e` as the only scheduling-specific pytest marker + +If you change CI bootstrap: + +- preserve the `LANGFUSE_INIT_*` startup path for the Langfuse server unless there is a strong reason to change it +- preserve `cancel-in-progress: true` + +## Repo Rules + +- Keep changes scoped. Avoid unrelated refactors. +- Prefer `LANGFUSE_BASE_URL`; `LANGFUSE_HOST` is deprecated and is only kept for compatibility tests. +- If you touch `langfuse/api/`, regenerate it from the upstream Fern/OpenAPI source instead of hand-editing files. +- If you change public SDK behavior, update examples, README snippets, or generated reference docs when they would otherwise become stale. +- If you touch shutdown, flushing, or worker-thread behavior, run the relevant resource-manager and OTEL-heavy tests. +- If you change OpenAI or LangChain instrumentation, keep as much coverage as possible in `tests/unit` using exporter-local assertions, and leave only the minimal necessary coverage in `tests/e2e` / `tests/live_provider`. +- Never commit secrets or credentials. +- Keep `.env.template` in sync with required local-development environment variables. + +## Commit And PR Rules + +- Commit messages and PR titles must follow Conventional Commits: `type(scope): description` or `type: description`. +- Allowed common types include `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`, and `security`. +- Keep commits focused and atomic. +- Before opening a PR, self-review the diff and check `code_review.md` for the repo-specific review checklist. +- In PR descriptions, list the main verification commands you ran and call out any skipped checks with the reason. + +## Python-Specific Notes + +- Exception messages should not inline f-string literals in the `raise` statement. Build the message in a variable first. +- Prefer ASCII-only edits unless the file already uses Unicode or Unicode is clearly required. + +## Release And Docs + +```bash +uv build --no-sources +uv run --group docs pdoc -o docs/ --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse +``` + +Releases are handled by GitHub Actions. Do not build an ad hoc local release flow into repository instructions. + +## External Docs + +- Prefer official documentation first when answering product or API questions. +- For OpenAI API, ChatGPT Apps SDK, or Codex questions, use the official OpenAI developer docs or Docs MCP server if available. + +## Git Safety + +- Do not use destructive git commands such as `git reset --hard` unless explicitly requested. +- Do not revert unrelated working-tree changes. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index b0f36e8d6..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,133 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is the Langfuse Python SDK, a client library for accessing the Langfuse observability platform. The SDK provides integration with OpenTelemetry (OTel) for tracing, automatic instrumentation for popular LLM frameworks (OpenAI, Langchain, etc.), and direct API access to Langfuse's features. - -## Development Commands - -### Setup -```bash -# Install Poetry plugins (one-time setup) -poetry self add poetry-dotenv-plugin -poetry self add poetry-bumpversion - -# Install all dependencies including optional extras -poetry install --all-extras - -# Setup pre-commit hooks -poetry run pre-commit install -``` - -### Testing -```bash -# Run all tests with verbose output -poetry run pytest -s -v --log-cli-level=INFO - -# Run a specific test -poetry run pytest -s -v --log-cli-level=INFO tests/test_core_sdk.py::test_flush - -# Run tests in parallel (faster) -poetry run pytest -s -v --log-cli-level=INFO -n auto -``` - -### Code Quality -```bash -# Format code with Ruff -poetry run ruff format . - -# Run linting (development config) -poetry run ruff check . - -# Run type checking -poetry run mypy . - -# Run pre-commit hooks manually -poetry run pre-commit run --all-files -``` - -### Building and Releasing -```bash -# Build the package -poetry build - -# Run release script (handles versioning, building, tagging, and publishing) -poetry run release - -# Generate documentation -poetry run pdoc -o docs/ --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse -``` - -## Architecture - -### Core Components - -- **`langfuse/_client/`**: Main SDK implementation built on OpenTelemetry - - `client.py`: Core Langfuse client with OTel integration - - `span.py`: LangfuseSpan, LangfuseGeneration, LangfuseEvent classes - - `observe.py`: Decorator for automatic instrumentation - - `datasets.py`: Dataset management functionality - -- **`langfuse/api/`**: Auto-generated Fern API client - - Contains all API resources and types - - Generated from OpenAPI spec - do not manually edit these files - -- **`langfuse/_task_manager/`**: Background processing - - Media upload handling and queue management - - Score ingestion consumer - -- **Integration modules**: - - `langfuse/openai.py`: OpenAI instrumentation - - `langfuse/langchain/`: Langchain integration via CallbackHandler - -### Key Design Patterns - -The SDK is built on OpenTelemetry for observability, using: -- Spans for tracing LLM operations -- Attributes for metadata (see `LangfuseOtelSpanAttributes`) -- Resource management for efficient batching and flushing - -The client follows an async-first design with automatic batching of events and background flushing to the Langfuse API. - -## Configuration - -Environment variables (defined in `_client/environment_variables.py`): -- `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY`: API credentials -- `LANGFUSE_HOST`: API endpoint (defaults to https://cloud.langfuse.com) -- `LANGFUSE_DEBUG`: Enable debug logging -- `LANGFUSE_TRACING_ENABLED`: Enable/disable tracing -- `LANGFUSE_SAMPLE_RATE`: Sampling rate for traces - -## Testing Notes - -- Create `.env` file based on `.env.template` for integration tests -- E2E tests with external APIs (OpenAI, SERP) are typically skipped in CI -- Remove `@pytest.mark.skip` decorators in test files to run external API tests -- Tests use `respx` for HTTP mocking and `pytest-httpserver` for test servers - -## Important Files - -- `pyproject.toml`: Poetry configuration, dependencies, and tool settings -- `ruff.toml`: Local development linting config (stricter) -- `ci.ruff.toml`: CI linting config (more permissive) -- `langfuse/version.py`: Version string (updated by release script) - -## API Generation - -The `langfuse/api/` directory is auto-generated from the Langfuse OpenAPI specification using Fern. To update: - -1. Generate new SDK in main Langfuse repo -2. Copy generated files from `generated/python` to `langfuse/api/` -3. Run `poetry run ruff format .` to format the generated code - -## Testing Guidelines - -### Approach to Test Changes -- Don't remove functionality from existing unit tests just to make tests pass. Only change the test, if underlying code changes warrant a test change. - -## Python Code Rules - -### Exception Handling -- Exception must not use an f-string literal, assign to variable first diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 62b490f33..45f1ed55d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,104 +2,141 @@ ## Development -### Add Poetry plugins +### Install dependencies +```bash +uv sync --locked ``` -poetry self add poetry-dotenv-plugin -poetry self add poetry-bumpversion + +### Add pre-commit + +```bash +uv run pre-commit install ``` -### Install dependencies +### Quality checks +```bash +uv run --frozen ruff check . +uv run --frozen ruff format . +uv run --frozen mypy langfuse --no-error-summary ``` -poetry install --all-extras + +For a broad local confidence check, run: + +```bash +bash scripts/codex/quick-check.sh ``` -### Add Pre-commit +### Tests + +Unit tests do not require a running Langfuse server: +```bash +uv run --frozen pytest -n auto --dist worksteal tests/unit ``` -poetry run pre-commit install + +E2E tests require a running Langfuse server and environment variables based on `.env.template`: + +```bash +uv run --frozen pytest -n 4 --dist worksteal tests/e2e -m "not serial_e2e" +uv run --frozen pytest tests/e2e -m "serial_e2e" ``` -### Tests +Live-provider tests make real provider calls and require provider API keys: -#### Setup +```bash +uv run --frozen pytest -n 4 --dist worksteal tests/live_provider -m "live_provider" +``` -- Add .env based on .env.template +Run a specific test with: -#### Run +```bash +uv run --frozen pytest tests/unit/test_resource_manager.py::test_pause_signals_score_consumer_shutdown +``` -- Run all +## Codex Cloud Setup - ``` - poetry run pytest -s -v --log-cli-level=INFO - ``` +This repository includes repo-owned Codex setup so agents can start from a reproducible environment. -- Run a specific test +Recommended Codex UI configuration: - ``` - poetry run pytest -s -v --log-cli-level=INFO tests/test_core_sdk.py::test_flush - ``` +1. Create a Codex cloud environment for this repository. +2. Set the setup script to: -- E2E tests involving OpenAI and Serp API are usually skipped, remove skip decorators in [tests/test_langchain.py](tests/test_langchain.py) to run them. + ```bash + bash scripts/codex/setup.sh + ``` -### Update openapi spec +3. Set the maintenance script to: -1. Generate Fern Python SDK in [langfuse](https://github.com/langfuse/langfuse) and copy the files generated in `generated/python` into the `langfuse/api` folder in this repo. -2. Execute the linter by running `poetry run ruff format .` -3. Rebuild and deploy the package to PyPi. + ```bash + bash scripts/codex/maintenance.sh + ``` -### Publish release +4. Keep agent internet access disabled by default, or allow only the domains required for the task. +5. Add secrets and environment variables in the Codex UI instead of committing them. -#### Release script +## Pull Requests -Make sure you have your PyPi API token setup in your poetry config. If not, you can set it up by running: +PR titles and commit messages must follow Conventional Commits: -```sh -poetry config pypi-token.pypi $your-api-token +```text +type(scope): description +type: description ``` -Run the release script: +Common types include `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`, and `security`. -```sh -poetry run release -``` +Before opening a PR: + +- Self-review the diff and use `code_review.md` for the repo-specific checklist. +- Keep changes focused and avoid unrelated refactors. +- Add or update tests for behavior changes. +- List the verification commands you ran in the PR description. + +### Update OpenAPI spec + +The generated API client in `langfuse/api/` must not be hand-edited. Regenerate it from the upstream Fern/OpenAPI source. + +### Publish release + +Releases are automated via GitHub Actions using PyPI Trusted Publishing (OIDC). + +To create a release: + +1. Go to [Actions > Release Python SDK](https://github.com/langfuse/langfuse-python/actions/workflows/release.yml) +2. Click "Run workflow" +3. Select the version bump type: + - `patch` - Bug fixes (1.0.0 → 1.0.1) + - `minor` - New features (1.0.0 → 1.1.0) + - `major` - Breaking changes (1.0.0 → 2.0.0) + - `prepatch`, `preminor`, or `premajor` - Pre-release versions (for example 1.0.0 → 1.0.1a1) +4. For pre-releases, select the type: `alpha`, `beta`, or `rc` +5. Click "Run workflow" -#### Manual steps (for prepatch versions) - -1. `poetry version patch` - - `poetry version prepatch` for pre-release versions -2. `poetry install` -3. `poetry build` -4. `git commit -am "chore: release v{version}"` -5. `git push` -6. `git tag v{version}` -7. `git push --tags` -8. `poetry publish` - - Create PyPi API token: - - Setup: `poetry config pypi-token.pypi your-api-token` -9. Create a release on GitHub with the changelog +The workflow will automatically: +- Bump the version in `pyproject.toml` +- Build the package +- Publish to PyPI +- Create a git tag and GitHub release with auto-generated release notes ### SDK Reference Note: The generated SDK reference is currently work in progress. -The SDK reference is generated via pdoc. You need to have all extra dependencies installed to generate the reference. - -```sh -poetry install --all-extras -``` +The SDK reference is generated via pdoc. The docs dependency group is installed on demand when you run the documentation commands. To update the reference, run the following command: ```sh -poetry run pdoc -o docs/ --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse +uv run --group docs pdoc -o docs/ --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse ``` To run the reference locally, you can use the following command: ```sh -poetry run pdoc --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse +uv run --group docs pdoc --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse ``` ## Credits diff --git a/README.md b/README.md index 94dc1c1d3..de79a88fb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Langfuse GitHub Banner](https://langfuse.com/langfuse_logo_white.png) +hero-b # Langfuse Python SDK @@ -12,7 +12,7 @@ ## Installation > [!IMPORTANT] -> The SDK was rewritten in v3 and released in June 2025. Refer to the [v3 migration guide](https://langfuse.com/docs/sdk/python/sdk-v3#upgrade-from-v2) for instructions on updating your code. +> The SDK was rewritten in v4 and released in March 2026. Refer to the [v4 migration guide](https://langfuse.com/docs/observability/sdk/upgrade-path/python-v3-to-v4) for instructions on updating your code. ``` pip install langfuse diff --git a/ci.ruff.toml b/ci.ruff.toml deleted file mode 100644 index 184e12e6f..000000000 --- a/ci.ruff.toml +++ /dev/null @@ -1,6 +0,0 @@ -# This is the Ruff config used in CI. -# In development, ruff.toml is used instead. - -target-version = 'py38' -[lint] -exclude = ["langfuse/api/**/*.py"] \ No newline at end of file diff --git a/code_review.md b/code_review.md new file mode 100644 index 000000000..a3c55051c --- /dev/null +++ b/code_review.md @@ -0,0 +1,38 @@ +# Langfuse Python SDK Review Checklist + +Use this checklist for `/review`, PR review, or self-review before handoff. + +## Priorities + +- Findings first: correctness bugs, regressions, security/privacy risks, performance issues with real impact, and missing tests for risky behavior. +- Keep line references tight and actionable. +- If no findings, say so explicitly and mention any residual risk or unrun verification. + +## SDK Correctness + +- Public SDK behavior should remain backwards compatible unless the PR is explicitly breaking. +- Prefer `LANGFUSE_BASE_URL`; `LANGFUSE_HOST` is deprecated and should only appear in compatibility paths or tests. +- Check shutdown, flushing, background task, and resource-manager changes for races, dropped events/scores/media, daemon-thread leaks, and hanging interpreter shutdown. +- OpenTelemetry changes should preserve context propagation, span parenting, exporter-local testability, and idempotent instrumentation setup. +- OpenAI and LangChain instrumentation should avoid brittle assertions on provider internals; prefer stable exporter-local behavior in unit tests. + +## API And Generated Code + +- Do not hand-edit `langfuse/api/`; regenerate it from the upstream Fern/OpenAPI source. +- Public API or serialization changes should include tests for request shape, response shape, and backwards-compatible aliases when relevant. +- Update README examples, `.env.template`, or generated reference docs when changed behavior would make them stale. + +## Tests And CI + +- Unit tests must not require a running Langfuse server. +- E2E tests should use bounded polling helpers from `tests/support/`, not raw `sleep()`. +- New e2e files must be named `tests/e2e/test_*.py` so mechanical CI sharding includes them. +- Use `serial_e2e` only for tests that are unsafe with shared-server concurrency. +- Live-provider tests should assert stable provider-facing behavior, not exact observation counts unless counts are the behavior under test. + +## Python Style + +- Exception messages should not inline f-string literals in `raise` statements; build the message in a variable first. +- Keep edits ASCII-only unless the file already uses Unicode or Unicode is clearly required. +- Keep changes scoped; avoid opportunistic refactors. +- Never commit secrets or credentials. diff --git a/langfuse/__init__.py b/langfuse/__init__.py index a61325071..08d8325cf 100644 --- a/langfuse/__init__.py +++ b/langfuse/__init__.py @@ -1,19 +1,76 @@ """.. include:: ../README.md""" +from langfuse.batch_evaluation import ( + BatchEvaluationResult, + BatchEvaluationResumeToken, + CompositeEvaluatorFunction, + EvaluatorInputs, + EvaluatorStats, + MapperFunction, +) +from langfuse.experiment import Evaluation, RegressionError, RunnerContext + +from ._client import client as _client_module from ._client.attributes import LangfuseOtelSpanAttributes +from ._client.constants import ObservationTypeLiteral from ._client.get_client import get_client -from ._client import client as _client from ._client.observe import observe -from ._client.span import LangfuseEvent, LangfuseGeneration, LangfuseSpan +from ._client.propagation import propagate_attributes +from ._client.span import ( + LangfuseAgent, + LangfuseChain, + LangfuseEmbedding, + LangfuseEvaluator, + LangfuseEvent, + LangfuseGeneration, + LangfuseGuardrail, + LangfuseRetriever, + LangfuseSpan, + LangfuseTool, +) +from ._version import __version__ +from .span_filter import ( + KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES, + is_default_export_span, + is_genai_span, + is_known_llm_instrumentor, + is_langfuse_span, +) -Langfuse = _client.Langfuse +Langfuse = _client_module.Langfuse __all__ = [ "Langfuse", "get_client", "observe", + "propagate_attributes", + "ObservationTypeLiteral", "LangfuseSpan", "LangfuseGeneration", "LangfuseEvent", "LangfuseOtelSpanAttributes", + "LangfuseAgent", + "LangfuseTool", + "LangfuseChain", + "LangfuseEmbedding", + "LangfuseEvaluator", + "LangfuseRetriever", + "LangfuseGuardrail", + "Evaluation", + "EvaluatorInputs", + "MapperFunction", + "CompositeEvaluatorFunction", + "EvaluatorStats", + "BatchEvaluationResumeToken", + "BatchEvaluationResult", + "RunnerContext", + "RegressionError", + "__version__", + "is_default_export_span", + "is_langfuse_span", + "is_genai_span", + "is_known_llm_instrumentor", + "KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES", + "experiment", + "api", ] diff --git a/langfuse/_client/attributes.py b/langfuse/_client/attributes.py index 1c22b7518..4660b50f0 100644 --- a/langfuse/_client/attributes.py +++ b/langfuse/_client/attributes.py @@ -12,11 +12,16 @@ import json from datetime import datetime -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, Literal, Optional, Union +from langfuse._client.constants import ( + ObservationTypeGenerationLike, + ObservationTypeSpanLike, +) from langfuse._utils.serializer import EventSerializer +from langfuse.api import MapValue from langfuse.model import PromptClient -from langfuse.types import MapValue, SpanLevel +from langfuse.types import SpanLevel class LangfuseOtelSpanAttributes: @@ -54,32 +59,30 @@ class LangfuseOtelSpanAttributes: # Internal AS_ROOT = "langfuse.internal.as_root" + IS_APP_ROOT = "langfuse.internal.is_app_root" + + # Experiments + EXPERIMENT_ID = "langfuse.experiment.id" + EXPERIMENT_NAME = "langfuse.experiment.name" + EXPERIMENT_DESCRIPTION = "langfuse.experiment.description" + EXPERIMENT_METADATA = "langfuse.experiment.metadata" + EXPERIMENT_DATASET_ID = "langfuse.experiment.dataset.id" + EXPERIMENT_ITEM_ID = "langfuse.experiment.item.id" + EXPERIMENT_ITEM_EXPECTED_OUTPUT = "langfuse.experiment.item.expected_output" + EXPERIMENT_ITEM_METADATA = "langfuse.experiment.item.metadata" + EXPERIMENT_ITEM_ROOT_OBSERVATION_ID = "langfuse.experiment.item.root_observation_id" def create_trace_attributes( *, - name: Optional[str] = None, - user_id: Optional[str] = None, - session_id: Optional[str] = None, - version: Optional[str] = None, - release: Optional[str] = None, input: Optional[Any] = None, output: Optional[Any] = None, - metadata: Optional[Any] = None, - tags: Optional[List[str]] = None, public: Optional[bool] = None, ) -> dict: attributes = { - LangfuseOtelSpanAttributes.TRACE_NAME: name, - LangfuseOtelSpanAttributes.TRACE_USER_ID: user_id, - LangfuseOtelSpanAttributes.TRACE_SESSION_ID: session_id, - LangfuseOtelSpanAttributes.VERSION: version, - LangfuseOtelSpanAttributes.RELEASE: release, LangfuseOtelSpanAttributes.TRACE_INPUT: _serialize(input), LangfuseOtelSpanAttributes.TRACE_OUTPUT: _serialize(output), - LangfuseOtelSpanAttributes.TRACE_TAGS: tags, LangfuseOtelSpanAttributes.TRACE_PUBLIC: public, - **_flatten_and_serialize_metadata(metadata, "trace"), } return {k: v for k, v in attributes.items() if v is not None} @@ -93,9 +96,12 @@ def create_span_attributes( level: Optional[SpanLevel] = None, status_message: Optional[str] = None, version: Optional[str] = None, + observation_type: Optional[ + Union[ObservationTypeSpanLike, Literal["event"]] + ] = "span", ) -> dict: attributes = { - LangfuseOtelSpanAttributes.OBSERVATION_TYPE: "span", + LangfuseOtelSpanAttributes.OBSERVATION_TYPE: observation_type, LangfuseOtelSpanAttributes.OBSERVATION_LEVEL: level, LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE: status_message, LangfuseOtelSpanAttributes.VERSION: version, @@ -122,9 +128,10 @@ def create_generation_attributes( usage_details: Optional[Dict[str, int]] = None, cost_details: Optional[Dict[str, float]] = None, prompt: Optional[PromptClient] = None, + observation_type: Optional[ObservationTypeGenerationLike] = "generation", ) -> dict: attributes = { - LangfuseOtelSpanAttributes.OBSERVATION_TYPE: "generation", + LangfuseOtelSpanAttributes.OBSERVATION_TYPE: observation_type, LangfuseOtelSpanAttributes.OBSERVATION_LEVEL: level, LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE: status_message, LangfuseOtelSpanAttributes.VERSION: version, @@ -152,7 +159,36 @@ def create_generation_attributes( def _serialize(obj: Any) -> Optional[str]: - return json.dumps(obj, cls=EventSerializer) if obj is not None else None + if obj is None or isinstance(obj, str): + return obj + + return json.dumps(obj, cls=EventSerializer) + + +def _flatten_and_serialize_metadata_values( + metadata: Optional[Dict[str, Any]], +) -> Optional[Dict[str, str]]: + if metadata is None: + return None + + flattened_metadata: Dict[str, str] = {} + + def flatten_value(path: str, value: Any) -> None: + if isinstance(value, dict): + for nested_key, nested_value in value.items(): + flatten_value(f"{path}.{nested_key}", nested_value) + + return + + serialized_value = _serialize(value) + + if serialized_value is not None: + flattened_metadata[path] = serialized_value + + for key, value in metadata.items(): + flatten_value(str(key), value) + + return flattened_metadata def _flatten_and_serialize_metadata( diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index 0f80f1816..2f1c8d783 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -3,55 +3,128 @@ This module implements Langfuse's core observability functionality on top of the OpenTelemetry (OTel) standard. """ +import asyncio import logging import os import re import urllib.parse +import warnings from datetime import datetime from hashlib import sha256 from time import time_ns -from typing import Any, Dict, List, Literal, Optional, Union, cast, overload +from typing import ( + Any, + Callable, + Dict, + List, + Literal, + Optional, + Type, + Union, + cast, + overload, +) import backoff import httpx -from opentelemetry import trace +from opentelemetry import context as otel_context_api from opentelemetry import trace as otel_trace_api -from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.util._decorator import ( _AgnosticContextManager, _agnosticcontextmanager, ) from packaging.version import Version +from typing_extensions import deprecated -from langfuse._client.attributes import LangfuseOtelSpanAttributes -from langfuse._client.datasets import DatasetClient, DatasetItemClient +from langfuse._client.attributes import ( + LangfuseOtelSpanAttributes, + _flatten_and_serialize_metadata_values, + _serialize, +) +from langfuse._client.constants import ( + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ObservationTypeGenerationLike, + ObservationTypeLiteral, + ObservationTypeLiteralNoEvent, + ObservationTypeSpanLike, + get_observation_types_list, +) +from langfuse._client.datasets import DatasetClient from langfuse._client.environment_variables import ( + LANGFUSE_BASE_URL, LANGFUSE_DEBUG, LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, + LANGFUSE_RELEASE, LANGFUSE_SAMPLE_RATE, LANGFUSE_SECRET_KEY, LANGFUSE_TIMEOUT, LANGFUSE_TRACING_ENABLED, LANGFUSE_TRACING_ENVIRONMENT, ) +from langfuse._client.propagation import ( + PropagatedExperimentAttributes, + _detach_context_token_safely, + _propagate_attributes, + _set_langfuse_trace_id_in_baggage, +) from langfuse._client.resource_manager import LangfuseResourceManager from langfuse._client.span import ( + LangfuseAgent, + LangfuseChain, + LangfuseEmbedding, + LangfuseEvaluator, LangfuseEvent, LangfuseGeneration, + LangfuseGuardrail, + LangfuseRetriever, LangfuseSpan, + LangfuseTool, ) +from langfuse._client.utils import get_sha256_hash_hex, run_async_safely from langfuse._utils import _get_timestamp +from langfuse._utils.environment import get_common_release_envs from langfuse._utils.parse_error import handle_fern_exception from langfuse._utils.prompt_cache import PromptCache -from langfuse.api.resources.commons.errors.error import Error -from langfuse.api.resources.ingestion.types.score_body import ScoreBody -from langfuse.api.resources.prompts.types import ( - CreatePromptRequest_Chat, - CreatePromptRequest_Text, +from langfuse.api import ( + CreateChatPromptRequest, + CreateChatPromptType, + CreateTextPromptRequest, + Dataset, + DatasetItem, + DatasetRunWithItems, + DatasetStatus, + DeleteDatasetRunResponse, + Error, + MapValue, + NotFoundError, + PaginatedDatasetRuns, Prompt_Chat, Prompt_Text, + ScoreBody, + TraceBody, +) +from langfuse.batch_evaluation import ( + BatchEvaluationResult, + BatchEvaluationResumeToken, + BatchEvaluationRunner, + CompositeEvaluatorFunction, + MapperFunction, +) +from langfuse.experiment import ( + Evaluation, + EvaluatorFunction, + ExperimentData, + ExperimentItem, + ExperimentItemResult, + ExperimentResult, + RunEvaluatorFunction, + TaskFunction, + _run_evaluator, + _run_task, ) from langfuse.logger import langfuse_logger from langfuse.media import LangfuseMedia @@ -59,12 +132,6 @@ ChatMessageDict, ChatMessageWithPlaceholdersDict, ChatPromptClient, - CreateDatasetItemRequest, - CreateDatasetRequest, - Dataset, - DatasetItem, - DatasetStatus, - MapValue, PromptClient, TextPromptClient, ) @@ -88,12 +155,13 @@ class Langfuse: Attributes: api: Synchronous API client for Langfuse backend communication async_api: Asynchronous API client for Langfuse backend communication - langfuse_tracer: Internal LangfuseTracer instance managing OpenTelemetry components + _otel_tracer: Internal LangfuseTracer instance managing OpenTelemetry components Parameters: public_key (Optional[str]): Your Langfuse public API key. Can also be set via LANGFUSE_PUBLIC_KEY environment variable. secret_key (Optional[str]): Your Langfuse secret API key. Can also be set via LANGFUSE_SECRET_KEY environment variable. - host (Optional[str]): The Langfuse API host URL. Defaults to "https://cloud.langfuse.com". Can also be set via LANGFUSE_HOST environment variable. + base_url (Optional[str]): The Langfuse API base URL. Defaults to "https://cloud.langfuse.com". Can also be set via LANGFUSE_BASE_URL environment variable. + host (Optional[str]): Deprecated. Use base_url instead. The Langfuse API host URL. Defaults to "https://cloud.langfuse.com". timeout (Optional[int]): Timeout in seconds for API requests. Defaults to 5 seconds. httpx_client (Optional[httpx.Client]): Custom httpx client for making non-tracing HTTP requests. If not provided, a default client will be created. debug (bool): Enable debug logging. Defaults to False. Can also be set via LANGFUSE_DEBUG environment variable. @@ -105,9 +173,23 @@ class Langfuse: media_upload_thread_count (Optional[int]): Number of background threads for handling media uploads. Defaults to 1. Can also be set via LANGFUSE_MEDIA_UPLOAD_THREAD_COUNT environment variable. sample_rate (Optional[float]): Sampling rate for traces (0.0 to 1.0). Defaults to 1.0 (100% of traces are sampled). Can also be set via LANGFUSE_SAMPLE_RATE environment variable. mask (Optional[MaskFunction]): Function to mask sensitive data in traces before sending to the API. - blocked_instrumentation_scopes (Optional[List[str]]): List of instrumentation scope names to block from being exported to Langfuse. Spans from these scopes will be filtered out before being sent to the API. Useful for filtering out spans from specific libraries or frameworks. For exported spans, you can see the instrumentation scope name in the span metadata in Langfuse (`metadata.scope.name`) - additional_headers (Optional[Dict[str, str]]): Additional headers to include in all API requests and OTLPSpanExporter requests. These headers will be merged with default headers. Note: If httpx_client is provided, additional_headers must be set directly on your custom httpx_client as well. + blocked_instrumentation_scopes (Optional[List[str]]): Deprecated. Use `should_export_span` instead. Equivalent behavior: + ```python + from langfuse.span_filter import is_default_export_span + blocked = {"sqlite", "requests"} + + should_export_span = lambda span: ( + is_default_export_span(span) + and ( + span.instrumentation_scope is None + or span.instrumentation_scope.name not in blocked + ) + ) + ``` + should_export_span (Optional[Callable[[ReadableSpan], bool]]): Callback to decide whether to export a span. If omitted, Langfuse uses the default filter (Langfuse SDK spans, spans with `gen_ai.*` attributes, and known LLM instrumentation scopes). + additional_headers (Optional[Dict[str, str]]): Additional headers to include in all API requests and in the default OTLPSpanExporter requests. These headers will be merged with default headers. Note: If httpx_client is provided, additional_headers must be set directly on your custom httpx_client as well. If `span_exporter` is provided, these headers are not wired into that exporter and must be configured on the exporter instance directly. tracer_provider(Optional[TracerProvider]): OpenTelemetry TracerProvider to use for Langfuse. This can be useful to set to have disconnected tracing between Langfuse and other OpenTelemetry-span emitting libraries. Note: To track active spans, the context is still shared between TracerProviders. This may lead to broken trace trees. + span_exporter (Optional[SpanExporter]): Custom OpenTelemetry span exporter for the Langfuse span processor. If omitted, Langfuse creates an OTLPSpanExporter pointed at the Langfuse OTLP endpoint. If provided, Langfuse does not wire `base_url`, exporter headers, exporter auth, or exporter timeout into it. Configure endpoint, headers, and timeout on the exporter instance directly. If you are sending spans to Langfuse v4 or using Langfuse Cloud Fast Preview, include `x-langfuse-ingestion-version=4` on the exporter to enable real time processing of exported spans. Example: ```python @@ -121,7 +203,7 @@ class Langfuse: ) # Create a trace span - with langfuse.start_as_current_span(name="process-query") as span: + with langfuse.start_as_current_observation(name="process-query") as span: # Your application code here # Create a nested generation span for an LLM call @@ -154,6 +236,7 @@ def __init__( *, public_key: Optional[str] = None, secret_key: Optional[str] = None, + base_url: Optional[str] = None, host: Optional[str] = None, timeout: Optional[int] = None, httpx_client: Optional[httpx.Client] = None, @@ -167,15 +250,25 @@ def __init__( sample_rate: Optional[float] = None, mask: Optional[MaskFunction] = None, blocked_instrumentation_scopes: Optional[List[str]] = None, + should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, additional_headers: Optional[Dict[str, str]] = None, tracer_provider: Optional[TracerProvider] = None, + span_exporter: Optional[SpanExporter] = None, ): - self._host = host or cast( - str, os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com") + self._base_url = ( + base_url + or os.environ.get(LANGFUSE_BASE_URL) + or host + or os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com") ) self._environment = environment or cast( str, os.environ.get(LANGFUSE_TRACING_ENVIRONMENT) ) + self._release = ( + release + or os.environ.get(LANGFUSE_RELEASE, None) + or get_common_release_envs() + ) self._project_id: Optional[str] = None sample_rate = sample_rate or float(os.environ.get(LANGFUSE_SAMPLE_RATE, 1.0)) if not 0.0 <= sample_rate <= 1.0: @@ -187,14 +280,16 @@ def __init__( self._tracing_enabled = ( tracing_enabled - and os.environ.get(LANGFUSE_TRACING_ENABLED, "True") != "False" + and os.environ.get(LANGFUSE_TRACING_ENABLED, "true").lower() != "false" ) if not self._tracing_enabled: langfuse_logger.info( "Configuration: Langfuse tracing is explicitly disabled. No data will be sent to the Langfuse API." ) - debug = debug if debug else (os.getenv(LANGFUSE_DEBUG, "False") == "True") + debug = ( + debug if debug else (os.getenv(LANGFUSE_DEBUG, "false").lower() == "true") + ) if debug: logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -219,13 +314,30 @@ def __init__( self._otel_tracer = otel_trace_api.NoOpTracer() return + if os.environ.get("OTEL_SDK_DISABLED", "false").lower() == "true": + langfuse_logger.warning( + "OTEL_SDK_DISABLED is set. Langfuse tracing will be disabled and no traces will appear in the UI." + ) + + if blocked_instrumentation_scopes is not None: + warnings.warn( + "`blocked_instrumentation_scopes` is deprecated and will be removed in a future release. " + "Use `should_export_span` instead. Example: " + "from langfuse.span_filter import is_default_export_span; " + 'blocked={"scope"}; should_export_span=lambda span: ' + "is_default_export_span(span) and (span.instrumentation_scope is None or " + "span.instrumentation_scope.name not in blocked).", + DeprecationWarning, + stacklevel=2, + ) + # Initialize api and tracer if requirements are met self._resources = LangfuseResourceManager( public_key=public_key, secret_key=secret_key, - host=self._host, + base_url=self._base_url, timeout=timeout, - environment=environment, + environment=self._environment, release=release, flush_at=flush_at, flush_interval=flush_interval, @@ -235,8 +347,10 @@ def __init__( mask=mask, tracing_enabled=self._tracing_enabled, blocked_instrumentation_scopes=blocked_instrumentation_scopes, + should_export_span=should_export_span, additional_headers=additional_headers, tracer_provider=tracer_provider, + span_exporter=span_exporter, ) self._mask = self._resources.mask @@ -248,183 +362,124 @@ def __init__( self.api = self._resources.api self.async_api = self._resources.async_api - def start_span( + @overload + def start_observation( self, *, trace_context: Optional[TraceContext] = None, name: str, + as_type: Literal["generation"], input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, - ) -> LangfuseSpan: - """Create a new span for tracing a unit of work. - - This method creates a new span but does not set it as the current span in the - context. To create and use a span within a context, use start_as_current_span(). - - The created span will be the child of the current span in the context. - - Args: - trace_context: Optional context for connecting to an existing trace - name: Name of the span (e.g., function or operation name) - input: Input data for the operation (can be any JSON-serializable object) - output: Output data from the operation (can be any JSON-serializable object) - metadata: Additional metadata to associate with the span - version: Version identifier for the code or component - level: Importance level of the span (info, warning, error) - status_message: Optional status message for the span - - Returns: - A LangfuseSpan object that must be ended with .end() when the operation completes - - Example: - ```python - span = langfuse.start_span(name="process-data") - try: - # Do work - span.update(output="result") - finally: - span.end() - ``` - """ - if trace_context: - trace_id = trace_context.get("trace_id", None) - parent_span_id = trace_context.get("parent_span_id", None) - - if trace_id: - remote_parent_span = self._create_remote_parent_span( - trace_id=trace_id, parent_span_id=parent_span_id - ) - - with otel_trace_api.use_span( - cast(otel_trace_api.Span, remote_parent_span) - ): - otel_span = self._otel_tracer.start_span(name=name) - otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) - - return LangfuseSpan( - otel_span=otel_span, - langfuse_client=self, - environment=self._environment, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) - - otel_span = self._otel_tracer.start_span(name=name) - - return LangfuseSpan( - otel_span=otel_span, - langfuse_client=self, - environment=self._environment, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + ) -> LangfuseGeneration: ... - def start_as_current_span( + @overload + def start_observation( self, *, trace_context: Optional[TraceContext] = None, name: str, + as_type: Literal["span"] = "span", input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, - end_on_exit: Optional[bool] = None, - ) -> _AgnosticContextManager[LangfuseSpan]: - """Create a new span and set it as the current span in a context manager. - - This method creates a new span and sets it as the current span within a context - manager. Use this method with a 'with' statement to automatically handle span - lifecycle within a code block. + ) -> LangfuseSpan: ... - The created span will be the child of the current span in the context. - - Args: - trace_context: Optional context for connecting to an existing trace - name: Name of the span (e.g., function or operation name) - input: Input data for the operation (can be any JSON-serializable object) - output: Output data from the operation (can be any JSON-serializable object) - metadata: Additional metadata to associate with the span - version: Version identifier for the code or component - level: Importance level of the span (info, warning, error) - status_message: Optional status message for the span - end_on_exit (default: True): Whether to end the span automatically when leaving the context manager. If False, the span must be manually ended to avoid memory leaks. - - Returns: - A context manager that yields a LangfuseSpan - - Example: - ```python - with langfuse.start_as_current_span(name="process-query") as span: - # Do work - result = process_data() - span.update(output=result) + @overload + def start_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["agent"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> LangfuseAgent: ... - # Create a child span automatically - with span.start_as_current_span(name="sub-operation") as child_span: - # Do sub-operation work - child_span.update(output="sub-result") - ``` - """ - if trace_context: - trace_id = trace_context.get("trace_id", None) - parent_span_id = trace_context.get("parent_span_id", None) + @overload + def start_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["tool"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> LangfuseTool: ... - if trace_id: - remote_parent_span = self._create_remote_parent_span( - trace_id=trace_id, parent_span_id=parent_span_id - ) + @overload + def start_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["chain"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> LangfuseChain: ... - return cast( - _AgnosticContextManager[LangfuseSpan], - self._create_span_with_parent_context( - as_type="span", - name=name, - remote_parent_span=remote_parent_span, - parent=None, - end_on_exit=end_on_exit, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ), - ) + @overload + def start_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["retriever"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> LangfuseRetriever: ... - return cast( - _AgnosticContextManager[LangfuseSpan], - self._start_as_current_otel_span_with_processed_media( - as_type="span", - name=name, - end_on_exit=end_on_exit, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ), - ) + @overload + def start_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["evaluator"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> LangfuseEvaluator: ... - def start_generation( + @overload + def start_observation( self, *, trace_context: Optional[TraceContext] = None, name: str, + as_type: Literal["embedding"], input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, @@ -437,56 +492,76 @@ def start_generation( usage_details: Optional[Dict[str, int]] = None, cost_details: Optional[Dict[str, float]] = None, prompt: Optional[PromptClient] = None, - ) -> LangfuseGeneration: - """Create a new generation span for model generations. + ) -> LangfuseEmbedding: ... - This method creates a specialized span for tracking model generations. - It includes additional fields specific to model generations such as model name, - token usage, and cost details. + @overload + def start_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["guardrail"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> LangfuseGuardrail: ... - The created generation span will be the child of the current span in the context. + def start_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: ObservationTypeLiteralNoEvent = "span", + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + ) -> Union[ + LangfuseSpan, + LangfuseGeneration, + LangfuseAgent, + LangfuseTool, + LangfuseChain, + LangfuseRetriever, + LangfuseEvaluator, + LangfuseEmbedding, + LangfuseGuardrail, + ]: + """Create a new observation of the specified type. + + This method creates a new observation but does not set it as the current span in the + context. To create and use an observation within a context, use start_as_current_observation(). Args: trace_context: Optional context for connecting to an existing trace - name: Name of the generation operation - input: Input data for the model (e.g., prompts) - output: Output from the model (e.g., completions) - metadata: Additional metadata to associate with the generation - version: Version identifier for the model or component - level: Importance level of the generation (info, warning, error) - status_message: Optional status message for the generation - completion_start_time: When the model started generating the response - model: Name/identifier of the AI model used (e.g., "gpt-4") - model_parameters: Parameters used for the model (e.g., temperature, max_tokens) - usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) - cost_details: Cost information for the model call - prompt: Associated prompt template from Langfuse prompt management + name: Name of the observation + as_type: Type of observation to create (defaults to "span") + input: Input data for the operation + output: Output data from the operation + metadata: Additional metadata to associate with the observation + version: Version identifier for the code or component + level: Importance level of the observation + status_message: Optional status message for the observation + completion_start_time: When the model started generating (for generation types) + model: Name/identifier of the AI model used (for generation types) + model_parameters: Parameters used for the model (for generation types) + usage_details: Token usage information (for generation types) + cost_details: Cost information (for generation types) + prompt: Associated prompt template (for generation types) Returns: - A LangfuseGeneration object that must be ended with .end() when complete - - Example: - ```python - generation = langfuse.start_generation( - name="answer-generation", - model="gpt-4", - input={"prompt": "Explain quantum computing"}, - model_parameters={"temperature": 0.7} - ) - try: - # Call model API - response = llm.generate(...) - - generation.update( - output=response.text, - usage_details={ - "prompt_tokens": response.usage.prompt_tokens, - "completion_tokens": response.usage.completion_tokens - } - ) - finally: - generation.end() - ``` + An observation object of the appropriate type that must be ended with .end() """ if trace_context: trace_id = trace_context.get("trace_id", None) @@ -503,9 +578,9 @@ def start_generation( otel_span = self._otel_tracer.start_span(name=name) otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) - return LangfuseGeneration( + return self._create_observation_from_otel_span( otel_span=otel_span, - langfuse_client=self, + as_type=as_type, input=input, output=output, metadata=metadata, @@ -522,9 +597,9 @@ def start_generation( otel_span = self._otel_tracer.start_span(name=name) - return LangfuseGeneration( + return self._create_observation_from_otel_span( otel_span=otel_span, - langfuse_client=self, + as_type=as_type, input=input, output=output, metadata=metadata, @@ -539,11 +614,203 @@ def start_generation( prompt=prompt, ) - def start_as_current_generation( + def _create_observation_from_otel_span( + self, + *, + otel_span: otel_trace_api.Span, + as_type: ObservationTypeLiteralNoEvent, + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + ) -> Union[ + LangfuseSpan, + LangfuseGeneration, + LangfuseAgent, + LangfuseTool, + LangfuseChain, + LangfuseRetriever, + LangfuseEvaluator, + LangfuseEmbedding, + LangfuseGuardrail, + ]: + """Create the appropriate observation type from an OTEL span.""" + if as_type in get_observation_types_list(ObservationTypeGenerationLike): + observation_class = self._get_span_class(as_type) + # Type ignore to prevent overloads of internal _get_span_class function, + # issue is that LangfuseEvent could be returned and that classes have diff. args + return observation_class( # type: ignore[return-value,call-arg] + otel_span=otel_span, + langfuse_client=self, + environment=self._environment, + release=self._release, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, + ) + else: + # For other types (e.g. span, guardrail), create appropriate class without generation properties + observation_class = self._get_span_class(as_type) + # Type ignore to prevent overloads of internal _get_span_class function, + # issue is that LangfuseEvent could be returned and that classes have diff. args + return observation_class( # type: ignore[return-value,call-arg] + otel_span=otel_span, + langfuse_client=self, + environment=self._environment, + release=self._release, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, + ) + # span._observation_type = as_type + # span._otel_span.set_attribute("langfuse.observation.type", as_type) + # return span + + @overload + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["generation"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + end_on_exit: Optional[bool] = None, + ) -> _AgnosticContextManager[LangfuseGeneration]: ... + + @overload + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["span"] = "span", + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + end_on_exit: Optional[bool] = None, + ) -> _AgnosticContextManager[LangfuseSpan]: ... + + @overload + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["agent"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + end_on_exit: Optional[bool] = None, + ) -> _AgnosticContextManager[LangfuseAgent]: ... + + @overload + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["tool"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + end_on_exit: Optional[bool] = None, + ) -> _AgnosticContextManager[LangfuseTool]: ... + + @overload + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["chain"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + end_on_exit: Optional[bool] = None, + ) -> _AgnosticContextManager[LangfuseChain]: ... + + @overload + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["retriever"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + end_on_exit: Optional[bool] = None, + ) -> _AgnosticContextManager[LangfuseRetriever]: ... + + @overload + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["evaluator"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + end_on_exit: Optional[bool] = None, + ) -> _AgnosticContextManager[LangfuseEvaluator]: ... + + @overload + def start_as_current_observation( self, *, trace_context: Optional[TraceContext] = None, name: str, + as_type: Literal["embedding"], input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, @@ -557,108 +824,290 @@ def start_as_current_generation( cost_details: Optional[Dict[str, float]] = None, prompt: Optional[PromptClient] = None, end_on_exit: Optional[bool] = None, - ) -> _AgnosticContextManager[LangfuseGeneration]: - """Create a new generation span and set it as the current span in a context manager. + ) -> _AgnosticContextManager[LangfuseEmbedding]: ... + + @overload + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: Literal["guardrail"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + end_on_exit: Optional[bool] = None, + ) -> _AgnosticContextManager[LangfuseGuardrail]: ... - This method creates a specialized span for model generations and sets it as the + def start_as_current_observation( + self, + *, + trace_context: Optional[TraceContext] = None, + name: str, + as_type: ObservationTypeLiteralNoEvent = "span", + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + end_on_exit: Optional[bool] = None, + ) -> Union[ + _AgnosticContextManager[LangfuseGeneration], + _AgnosticContextManager[LangfuseSpan], + _AgnosticContextManager[LangfuseAgent], + _AgnosticContextManager[LangfuseTool], + _AgnosticContextManager[LangfuseChain], + _AgnosticContextManager[LangfuseRetriever], + _AgnosticContextManager[LangfuseEvaluator], + _AgnosticContextManager[LangfuseEmbedding], + _AgnosticContextManager[LangfuseGuardrail], + ]: + """Create a new observation and set it as the current span in a context manager. + + This method creates a new observation of the specified type and sets it as the current span within a context manager. Use this method with a 'with' statement to - automatically handle the generation span lifecycle within a code block. + automatically handle the observation lifecycle within a code block. - The created generation span will be the child of the current span in the context. + The created observation will be the child of the current span in the context. Args: trace_context: Optional context for connecting to an existing trace - name: Name of the generation operation - input: Input data for the model (e.g., prompts) - output: Output from the model (e.g., completions) - metadata: Additional metadata to associate with the generation - version: Version identifier for the model or component - level: Importance level of the generation (info, warning, error) - status_message: Optional status message for the generation + name: Name of the observation (e.g., function or operation name) + as_type: Type of observation to create (defaults to "span") + input: Input data for the operation (can be any JSON-serializable object) + output: Output data from the operation (can be any JSON-serializable object) + metadata: Additional metadata to associate with the observation + version: Version identifier for the code or component + level: Importance level of the observation (info, warning, error) + status_message: Optional status message for the observation + end_on_exit (default: True): Whether to end the span automatically when leaving the context manager. If False, the span must be manually ended to avoid memory leaks. + + The following parameters are available when as_type is: "generation" or "embedding". completion_start_time: When the model started generating the response model: Name/identifier of the AI model used (e.g., "gpt-4") model_parameters: Parameters used for the model (e.g., temperature, max_tokens) usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) cost_details: Cost information for the model call prompt: Associated prompt template from Langfuse prompt management - end_on_exit (default: True): Whether to end the span automatically when leaving the context manager. If False, the span must be manually ended to avoid memory leaks. Returns: - A context manager that yields a LangfuseGeneration + A context manager that yields the appropriate observation type based on as_type Example: ```python - with langfuse.start_as_current_generation( + # Create a span + with langfuse.start_as_current_observation(name="process-query", as_type="span") as span: + # Do work + result = process_data() + span.update(output=result) + + # Create a child span automatically + with span.start_as_current_observation(name="sub-operation") as child_span: + # Do sub-operation work + child_span.update(output="sub-result") + + # Create a tool observation + with langfuse.start_as_current_observation(name="web-search", as_type="tool") as tool: + # Do tool work + results = search_web(query) + tool.update(output=results) + + # Create a generation observation + with langfuse.start_as_current_observation( name="answer-generation", - model="gpt-4", - input={"prompt": "Explain quantum computing"} + as_type="generation", + model="gpt-4" ) as generation: - # Call model API + # Generate answer response = llm.generate(...) - - # Update with results - generation.update( - output=response.text, - usage_details={ - "prompt_tokens": response.usage.prompt_tokens, - "completion_tokens": response.usage.completion_tokens - } - ) + generation.update(output=response) ``` """ - if trace_context: - trace_id = trace_context.get("trace_id", None) - parent_span_id = trace_context.get("parent_span_id", None) + if as_type in get_observation_types_list(ObservationTypeGenerationLike): + if trace_context: + trace_id = trace_context.get("trace_id", None) + parent_span_id = trace_context.get("parent_span_id", None) + + if trace_id: + remote_parent_span = self._create_remote_parent_span( + trace_id=trace_id, parent_span_id=parent_span_id + ) - if trace_id: - remote_parent_span = self._create_remote_parent_span( - trace_id=trace_id, parent_span_id=parent_span_id - ) + return cast( + Union[ + _AgnosticContextManager[LangfuseGeneration], + _AgnosticContextManager[LangfuseEmbedding], + ], + self._create_span_with_parent_context( + as_type=as_type, + name=name, + remote_parent_span=remote_parent_span, + parent=None, + end_on_exit=end_on_exit, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, + ), + ) - return cast( + return cast( + Union[ _AgnosticContextManager[LangfuseGeneration], - self._create_span_with_parent_context( - as_type="generation", - name=name, - remote_parent_span=remote_parent_span, - parent=None, - end_on_exit=end_on_exit, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, - ), - ) + _AgnosticContextManager[LangfuseEmbedding], + ], + self._start_as_current_otel_span_with_processed_media( + as_type=as_type, + name=name, + end_on_exit=end_on_exit, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, + ), + ) - return cast( - _AgnosticContextManager[LangfuseGeneration], - self._start_as_current_otel_span_with_processed_media( - as_type="generation", - name=name, - end_on_exit=end_on_exit, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, - ), + if as_type in get_observation_types_list(ObservationTypeSpanLike): + if trace_context: + trace_id = trace_context.get("trace_id", None) + parent_span_id = trace_context.get("parent_span_id", None) + + if trace_id: + remote_parent_span = self._create_remote_parent_span( + trace_id=trace_id, parent_span_id=parent_span_id + ) + + return cast( + Union[ + _AgnosticContextManager[LangfuseSpan], + _AgnosticContextManager[LangfuseAgent], + _AgnosticContextManager[LangfuseTool], + _AgnosticContextManager[LangfuseChain], + _AgnosticContextManager[LangfuseRetriever], + _AgnosticContextManager[LangfuseEvaluator], + _AgnosticContextManager[LangfuseGuardrail], + ], + self._create_span_with_parent_context( + as_type=as_type, + name=name, + remote_parent_span=remote_parent_span, + parent=None, + end_on_exit=end_on_exit, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, + ), + ) + + return cast( + Union[ + _AgnosticContextManager[LangfuseSpan], + _AgnosticContextManager[LangfuseAgent], + _AgnosticContextManager[LangfuseTool], + _AgnosticContextManager[LangfuseChain], + _AgnosticContextManager[LangfuseRetriever], + _AgnosticContextManager[LangfuseEvaluator], + _AgnosticContextManager[LangfuseGuardrail], + ], + self._start_as_current_otel_span_with_processed_media( + as_type=as_type, + name=name, + end_on_exit=end_on_exit, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, + ), + ) + + # This should never be reached since all valid types are handled above + langfuse_logger.warning( + f"Unknown observation type: {as_type}, falling back to span" + ) + return self._start_as_current_otel_span_with_processed_media( + as_type="span", + name=name, + end_on_exit=end_on_exit, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, ) + def _get_span_class( + self, + as_type: ObservationTypeLiteral, + ) -> Union[ + Type[LangfuseAgent], + Type[LangfuseTool], + Type[LangfuseChain], + Type[LangfuseRetriever], + Type[LangfuseEvaluator], + Type[LangfuseEmbedding], + Type[LangfuseGuardrail], + Type[LangfuseGeneration], + Type[LangfuseEvent], + Type[LangfuseSpan], + ]: + """Get the appropriate span class based on as_type.""" + normalized_type = as_type.lower() + + if normalized_type == "agent": + return LangfuseAgent + elif normalized_type == "tool": + return LangfuseTool + elif normalized_type == "chain": + return LangfuseChain + elif normalized_type == "retriever": + return LangfuseRetriever + elif normalized_type == "evaluator": + return LangfuseEvaluator + elif normalized_type == "embedding": + return LangfuseEmbedding + elif normalized_type == "guardrail": + return LangfuseGuardrail + elif normalized_type == "generation": + return LangfuseGeneration + elif normalized_type == "event": + return LangfuseEvent + elif normalized_type == "span": + return LangfuseSpan + else: + return LangfuseSpan + @_agnosticcontextmanager def _create_span_with_parent_context( self, @@ -666,7 +1115,7 @@ def _create_span_with_parent_context( name: str, parent: Optional[otel_trace_api.Span] = None, remote_parent_span: Optional[otel_trace_api.Span] = None, - as_type: Literal["generation", "span"], + as_type: ObservationTypeLiteralNoEvent, end_on_exit: Optional[bool] = None, input: Optional[Any] = None, output: Optional[Any] = None, @@ -713,7 +1162,7 @@ def _start_as_current_otel_span_with_processed_media( self, *, name: str, - as_type: Optional[Literal["generation", "span"]] = None, + as_type: Optional[ObservationTypeLiteralNoEvent] = None, end_on_exit: Optional[bool] = None, input: Optional[Any] = None, output: Optional[Any] = None, @@ -732,37 +1181,54 @@ def _start_as_current_otel_span_with_processed_media( name=name, end_on_exit=end_on_exit if end_on_exit is not None else True, ) as otel_span: - yield ( - LangfuseSpan( - otel_span=otel_span, - langfuse_client=self, - environment=self._environment, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) - if as_type == "span" - else LangfuseGeneration( - otel_span=otel_span, - langfuse_client=self, - environment=self._environment, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, + baggage_token = None + + if otel_span.is_recording(): + context_with_app_root_claim = _set_langfuse_trace_id_in_baggage( + trace_id=self._get_otel_trace_id(otel_span), + context=otel_context_api.get_current(), ) - ) + baggage_token = otel_context_api.attach(context_with_app_root_claim) + + span_class = self._get_span_class( + as_type or "generation" + ) # default was "generation" + + try: + common_args = { + "otel_span": otel_span, + "langfuse_client": self, + "environment": self._environment, + "release": self._release, + "input": input, + "output": output, + "metadata": metadata, + "version": version, + "level": level, + "status_message": status_message, + } + + if span_class in [ + LangfuseGeneration, + LangfuseEmbedding, + ]: + common_args.update( + { + "completion_start_time": completion_start_time, + "model": model, + "model_parameters": model_parameters, + "usage_details": usage_details, + "cost_details": cost_details, + "prompt": prompt, + } + ) + # For span-like types (span, agent, tool, chain, retriever, evaluator, guardrail), no generation properties needed + + yield span_class(**common_args) # type: ignore[arg-type] + + finally: + if baggage_token is not None: + _detach_context_token_safely(baggage_token) def _get_current_otel_span(self) -> Optional[otel_trace_api.Span]: current_span = otel_trace_api.get_current_span() @@ -770,7 +1236,7 @@ def _get_current_otel_span(self) -> Optional[otel_trace_api.Span]: if current_span is otel_trace_api.INVALID_SPAN: langfuse_logger.warning( "Context error: No active span in current context. Operations that depend on an active span will be skipped. " - "Ensure spans are created with start_as_current_span() or that you're operating within an active span context." + "Ensure spans are created with start_as_current_observation() or that you're operating within an active span context." ) return None @@ -889,7 +1355,7 @@ def update_current_span( Example: ```python - with langfuse.start_as_current_span(name="process-data") as span: + with langfuse.start_as_current_observation(name="process-data") as span: # Initial processing result = process_first_part() @@ -916,6 +1382,7 @@ def update_current_span( otel_span=current_otel_span, langfuse_client=self, environment=self._environment, + release=self._release, ) if name: @@ -930,83 +1397,90 @@ def update_current_span( status_message=status_message, ) - def update_current_trace( + @deprecated( + "Trace-level input/output is deprecated. " + "For trace attributes (user_id, session_id, tags, etc.), use propagate_attributes() instead. " + "This method will be removed in a future major version." + ) + def set_current_trace_io( self, *, - name: Optional[str] = None, - user_id: Optional[str] = None, - session_id: Optional[str] = None, - version: Optional[str] = None, input: Optional[Any] = None, output: Optional[Any] = None, - metadata: Optional[Any] = None, - tags: Optional[List[str]] = None, - public: Optional[bool] = None, ) -> None: - """Update the current trace with additional information. + """Set trace-level input and output for the current span's trace. - This method updates the Langfuse trace that the current span belongs to. It's useful for - adding trace-level metadata like user ID, session ID, or tags that apply to - the entire Langfuse trace rather than just a single observation. + .. deprecated:: + This is a legacy method for backward compatibility with Langfuse platform + features that still rely on trace-level input/output (e.g., legacy LLM-as-a-judge + evaluators). It will be removed in a future major version. - Args: - name: Updated name for the Langfuse trace - user_id: ID of the user who initiated the Langfuse trace - session_id: Session identifier for grouping related Langfuse traces - version: Version identifier for the application or service - input: Input data for the overall Langfuse trace - output: Output data from the overall Langfuse trace - metadata: Additional metadata to associate with the Langfuse trace - tags: List of tags to categorize the Langfuse trace - public: Whether the Langfuse trace should be publicly accessible - - Example: - ```python - with langfuse.start_as_current_span(name="handle-request") as span: - # Get user information - user = authenticate_user(request) - - # Update trace with user context - langfuse.update_current_trace( - user_id=user.id, - session_id=request.session_id, - tags=["production", "web-app"] - ) - - # Continue processing - response = process_request(request) + For setting other trace attributes (user_id, session_id, metadata, tags, version), + use :meth:`propagate_attributes` instead. - # Update span with results - span.update(output=response) - ``` + Args: + input: Input data to associate with the trace. + output: Output data to associate with the trace. """ if not self._tracing_enabled: langfuse_logger.debug( - "Operation skipped: update_current_trace - Tracing is disabled or client is in no-op mode." + "Operation skipped: set_current_trace_io - Tracing is disabled or client is in no-op mode." ) return current_otel_span = self._get_current_otel_span() - if current_otel_span is not None: - span = LangfuseSpan( + if current_otel_span is not None and current_otel_span.is_recording(): + existing_observation_type = current_otel_span.attributes.get( # type: ignore[attr-defined] + LangfuseOtelSpanAttributes.OBSERVATION_TYPE, "span" + ) + # We need to preserve the class to keep the correct observation type + span_class = self._get_span_class(existing_observation_type) + span = span_class( otel_span=current_otel_span, langfuse_client=self, environment=self._environment, + release=self._release, ) - span.update_trace( - name=name, - user_id=user_id, - session_id=session_id, - version=version, + span.set_trace_io( input=input, output=output, - metadata=metadata, - tags=tags, - public=public, ) + def set_current_trace_as_public(self) -> None: + """Make the current trace publicly accessible via its URL. + + When a trace is published, anyone with the trace link can view the full trace + without needing to be logged in to Langfuse. This action cannot be undone + programmatically - once published, the entire trace becomes public. + + This is a convenience method that publishes the trace from the currently + active span context. Use this when you want to make a trace public from + within a traced function without needing direct access to the span object. + """ + if not self._tracing_enabled: + langfuse_logger.debug( + "Operation skipped: set_current_trace_as_public - Tracing is disabled or client is in no-op mode." + ) + return + + current_otel_span = self._get_current_otel_span() + + if current_otel_span is not None and current_otel_span.is_recording(): + existing_observation_type = current_otel_span.attributes.get( # type: ignore[attr-defined] + LangfuseOtelSpanAttributes.OBSERVATION_TYPE, "span" + ) + # We need to preserve the class to keep the correct observation type + span_class = self._get_span_class(existing_observation_type) + span = span_class( + otel_span=current_otel_span, + langfuse_client=self, + environment=self._environment, + ) + + span.set_trace_as_public() + def create_event( self, *, @@ -1066,6 +1540,7 @@ def create_event( otel_span=otel_span, langfuse_client=self, environment=self._environment, + release=self._release, input=input, output=output, metadata=metadata, @@ -1083,6 +1558,7 @@ def create_event( otel_span=otel_span, langfuse_client=self, environment=self._environment, + release=self._release, input=input, output=output, metadata=metadata, @@ -1119,7 +1595,7 @@ def _create_remote_parent_span( is_remote=False, ) - return trace.NonRecordingSpan(span_context) + return otel_trace_api.NonRecordingSpan(span_context) def _is_valid_trace_id(self, trace_id: str) -> bool: pattern = r"^[0-9a-f]{32}$" @@ -1213,7 +1689,7 @@ def create_trace_id(*, seed: Optional[str] = None) -> str: correlated_trace_id = langfuse.create_trace_id(seed=external_id) # Use the ID with trace context - with langfuse.start_as_current_span( + with langfuse.start_as_current_observation( name="process-request", trace_context={"trace_id": trace_id} ) as span: @@ -1283,6 +1759,7 @@ def create_score( comment: Optional[str] = None, config_id: Optional[str] = None, metadata: Optional[Any] = None, + timestamp: Optional[datetime] = None, ) -> None: ... @overload @@ -1296,10 +1773,11 @@ def create_score( trace_id: Optional[str] = None, score_id: Optional[str] = None, observation_id: Optional[str] = None, - data_type: Optional[Literal["CATEGORICAL"]] = "CATEGORICAL", + data_type: Optional[Literal["CATEGORICAL", "TEXT"]] = "CATEGORICAL", comment: Optional[str] = None, config_id: Optional[str] = None, metadata: Optional[Any] = None, + timestamp: Optional[datetime] = None, ) -> None: ... def create_score( @@ -1316,6 +1794,7 @@ def create_score( comment: Optional[str] = None, config_id: Optional[str] = None, metadata: Optional[Any] = None, + timestamp: Optional[datetime] = None, ) -> None: """Create a score for a specific trace or observation. @@ -1324,16 +1803,17 @@ def create_score( Args: name: Name of the score (e.g., "relevance", "accuracy") - value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL) + value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT) session_id: ID of the Langfuse session to associate the score with dataset_run_id: ID of the Langfuse dataset run to associate the score with trace_id: ID of the Langfuse trace to associate the score with observation_id: Optional ID of the specific observation to score. Trace ID must be provided too. score_id: Optional custom ID for the score (auto-generated if not provided) - data_type: Type of score (NUMERIC, BOOLEAN, or CATEGORICAL) + data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, or TEXT) comment: Optional comment or explanation for the score config_id: Optional ID of a score config defined in Langfuse metadata: Optional metadata to be attached to the score + timestamp: Optional timestamp for the score (defaults to current UTC time) Example: ```python @@ -1379,25 +1859,58 @@ def create_score( event = { "id": self.create_trace_id(), - "type": "score-create", + "type": "score-create", + "timestamp": timestamp or _get_timestamp(), + "body": new_body, + } + + if self._resources is not None: + # Force the score to be in sample if it was for a legacy trace ID, i.e. non-32 hexchar + force_sample = ( + not self._is_valid_trace_id(trace_id) if trace_id else True + ) + + self._resources.add_score_task( + event, + force_sample=force_sample, + ) + + except Exception as e: + langfuse_logger.exception( + f"Error creating score: Failed to process score event for trace_id={trace_id}, name={name}. Error: {e}" + ) + + def _create_trace_tags_via_ingestion( + self, + *, + trace_id: str, + tags: List[str], + ) -> None: + """Private helper to enqueue trace tag updates via ingestion API events.""" + if not self._tracing_enabled: + return + + if len(tags) == 0: + return + + try: + new_body = TraceBody( + id=trace_id, + tags=tags, + ) + + event = { + "id": self.create_trace_id(), + "type": "trace-create", "timestamp": _get_timestamp(), "body": new_body, } if self._resources is not None: - # Force the score to be in sample if it was for a legacy trace ID, i.e. non-32 hexchar - force_sample = ( - not self._is_valid_trace_id(trace_id) if trace_id else True - ) - - self._resources.add_score_task( - event, - force_sample=force_sample, - ) - + self._resources.add_trace_task(event) except Exception as e: langfuse_logger.exception( - f"Error creating score: Failed to process score event for trace_id={trace_id}, name={name}. Error: {e}" + f"Error updating trace tags: Failed to process trace update event for trace_id={trace_id}. Error: {e}" ) @overload @@ -1410,6 +1923,7 @@ def score_current_span( data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None, comment: Optional[str] = None, config_id: Optional[str] = None, + metadata: Optional[Any] = None, ) -> None: ... @overload @@ -1419,9 +1933,10 @@ def score_current_span( name: str, value: str, score_id: Optional[str] = None, - data_type: Optional[Literal["CATEGORICAL"]] = "CATEGORICAL", + data_type: Optional[Literal["CATEGORICAL", "TEXT"]] = "CATEGORICAL", comment: Optional[str] = None, config_id: Optional[str] = None, + metadata: Optional[Any] = None, ) -> None: ... def score_current_span( @@ -1433,6 +1948,7 @@ def score_current_span( data_type: Optional[ScoreDataType] = None, comment: Optional[str] = None, config_id: Optional[str] = None, + metadata: Optional[Any] = None, ) -> None: """Create a score for the current active span. @@ -1441,11 +1957,12 @@ def score_current_span( Args: name: Name of the score (e.g., "relevance", "accuracy") - value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL) + value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT) score_id: Optional custom ID for the score (auto-generated if not provided) - data_type: Type of score (NUMERIC, BOOLEAN, or CATEGORICAL) + data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, or TEXT) comment: Optional comment or explanation for the score config_id: Optional ID of a score config defined in Langfuse + metadata: Optional metadata to be attached to the score Example: ```python @@ -1459,7 +1976,8 @@ def score_current_span( name="relevance", value=0.85, data_type="NUMERIC", - comment="Mostly relevant but contains some tangential information" + comment="Mostly relevant but contains some tangential information", + metadata={"model": "gpt-4", "prompt_version": "v2"} ) ``` """ @@ -1479,9 +1997,10 @@ def score_current_span( name=name, value=cast(str, value), score_id=score_id, - data_type=cast(Literal["CATEGORICAL"], data_type), + data_type=cast(Literal["CATEGORICAL", "TEXT"], data_type), comment=comment, config_id=config_id, + metadata=metadata, ) @overload @@ -1494,6 +2013,7 @@ def score_current_trace( data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None, comment: Optional[str] = None, config_id: Optional[str] = None, + metadata: Optional[Any] = None, ) -> None: ... @overload @@ -1503,9 +2023,10 @@ def score_current_trace( name: str, value: str, score_id: Optional[str] = None, - data_type: Optional[Literal["CATEGORICAL"]] = "CATEGORICAL", + data_type: Optional[Literal["CATEGORICAL", "TEXT"]] = "CATEGORICAL", comment: Optional[str] = None, config_id: Optional[str] = None, + metadata: Optional[Any] = None, ) -> None: ... def score_current_trace( @@ -1517,6 +2038,7 @@ def score_current_trace( data_type: Optional[ScoreDataType] = None, comment: Optional[str] = None, config_id: Optional[str] = None, + metadata: Optional[Any] = None, ) -> None: """Create a score for the current trace. @@ -1526,15 +2048,16 @@ def score_current_trace( Args: name: Name of the score (e.g., "user_satisfaction", "overall_quality") - value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL) + value: Score value (can be numeric for NUMERIC/BOOLEAN types or string for CATEGORICAL/TEXT) score_id: Optional custom ID for the score (auto-generated if not provided) - data_type: Type of score (NUMERIC, BOOLEAN, or CATEGORICAL) + data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, or TEXT) comment: Optional comment or explanation for the score config_id: Optional ID of a score config defined in Langfuse + metadata: Optional metadata to be attached to the score Example: ```python - with langfuse.start_as_current_span(name="process-user-request") as span: + with langfuse.start_as_current_observation(name="process-user-request") as span: # Process request result = process_complete_request() span.update(output=result) @@ -1544,7 +2067,8 @@ def score_current_trace( name="overall_quality", value=0.95, data_type="NUMERIC", - comment="High quality end-to-end response" + comment="High quality end-to-end response", + metadata={"evaluator": "gpt-4", "criteria": "comprehensive"} ) ``` """ @@ -1562,9 +2086,10 @@ def score_current_trace( name=name, value=cast(str, value), score_id=score_id, - data_type=cast(Literal["CATEGORICAL"], data_type), + data_type=cast(Literal["CATEGORICAL", "TEXT"], data_type), comment=comment, config_id=config_id, + metadata=metadata, ) def flush(self) -> None: @@ -1577,7 +2102,7 @@ def flush(self) -> None: Example: ```python # Record some spans and scores - with langfuse.start_as_current_span(name="operation") as span: + with langfuse.start_as_current_observation(name="operation") as span: # Do work... pass @@ -1628,7 +2153,7 @@ def get_current_trace_id(self) -> Optional[str]: Example: ```python - with langfuse.start_as_current_span(name="process-request") as span: + with langfuse.start_as_current_observation(name="process-request") as span: # Get the current trace ID for reference trace_id = langfuse.get_current_trace_id() @@ -1662,7 +2187,7 @@ def get_current_observation_id(self) -> Optional[str]: Example: ```python - with langfuse.start_as_current_span(name="process-user-query") as span: + with langfuse.start_as_current_observation(name="process-user-query") as span: # Get the current observation ID observation_id = langfuse.get_current_observation_id() @@ -1710,7 +2235,7 @@ def get_trace_url(self, *, trace_id: Optional[str] = None) -> Optional[str]: Example: ```python # Get URL for the current trace - with langfuse.start_as_current_span(name="process-request") as span: + with langfuse.start_as_current_observation(name="process-request") as span: trace_url = langfuse.get_trace_url() log.info(f"Processing trace: {trace_url}") @@ -1719,30 +2244,40 @@ def get_trace_url(self, *, trace_id: Optional[str] = None) -> Optional[str]: send_notification(f"Review needed for trace: {specific_trace_url}") ``` """ - project_id = self._get_project_id() final_trace_id = trace_id or self.get_current_trace_id() + if not final_trace_id: + return None + + project_id = self._get_project_id() return ( - f"{self._host}/project/{project_id}/traces/{final_trace_id}" + f"{self._base_url}/project/{project_id}/traces/{final_trace_id}" if project_id and final_trace_id else None ) def get_dataset( - self, name: str, *, fetch_items_page_size: Optional[int] = 50 + self, + name: str, + *, + fetch_items_page_size: Optional[int] = 50, + version: Optional[datetime] = None, ) -> "DatasetClient": """Fetch a dataset by its name. Args: name (str): The name of the dataset to fetch. fetch_items_page_size (Optional[int]): All items of the dataset will be fetched in chunks of this size. Defaults to 50. + version (Optional[datetime]): Retrieve dataset items as they existed at this specific point in time (UTC). + If provided, returns the state of items at the specified UTC timestamp. + If not provided, returns the latest version. Must be a timezone-aware datetime object in UTC. Returns: DatasetClient: The dataset with the given name. """ try: langfuse_logger.debug(f"Getting datasets {name}") - dataset = self.api.datasets.get(dataset_name=name) + dataset = self.api.datasets.get(dataset_name=self._url_encode(name)) dataset_items = [] page = 1 @@ -1752,6 +2287,7 @@ def get_dataset( dataset_name=self._url_encode(name, is_url_param=True), page=page, limit=fetch_items_page_size, + version=version, ) dataset_items.extend(new_items.data) @@ -1760,14 +2296,890 @@ def get_dataset( page += 1 - items = [DatasetItemClient(i, langfuse=self) for i in dataset_items] + return DatasetClient( + dataset=dataset, + items=dataset_items, + version=version, + langfuse_client=self, + ) + + except Error as e: + handle_fern_exception(e) + raise e + + def get_dataset_run( + self, *, dataset_name: str, run_name: str + ) -> DatasetRunWithItems: + """Fetch a dataset run by dataset name and run name. + + Args: + dataset_name (str): The name of the dataset. + run_name (str): The name of the run. + + Returns: + DatasetRunWithItems: The dataset run with its items. + """ + try: + return cast( + DatasetRunWithItems, + self.api.datasets.get_run( + dataset_name=self._url_encode(dataset_name), + run_name=self._url_encode(run_name), + request_options=None, + ), + ) + except Error as e: + handle_fern_exception(e) + raise e + + def get_dataset_runs( + self, + *, + dataset_name: str, + page: Optional[int] = None, + limit: Optional[int] = None, + ) -> PaginatedDatasetRuns: + """Fetch all runs for a dataset. + + Args: + dataset_name (str): The name of the dataset. + page (Optional[int]): Page number, starts at 1. + limit (Optional[int]): Limit of items per page. + + Returns: + PaginatedDatasetRuns: Paginated list of dataset runs. + """ + try: + return cast( + PaginatedDatasetRuns, + self.api.datasets.get_runs( + dataset_name=self._url_encode(dataset_name), + page=page, + limit=limit, + request_options=None, + ), + ) + except Error as e: + handle_fern_exception(e) + raise e + + def delete_dataset_run( + self, *, dataset_name: str, run_name: str + ) -> DeleteDatasetRunResponse: + """Delete a dataset run and all its run items. This action is irreversible. - return DatasetClient(dataset, items=items) + Args: + dataset_name (str): The name of the dataset. + run_name (str): The name of the run. + Returns: + DeleteDatasetRunResponse: Confirmation of deletion. + """ + try: + return cast( + DeleteDatasetRunResponse, + self.api.datasets.delete_run( + dataset_name=self._url_encode(dataset_name), + run_name=self._url_encode(run_name), + request_options=None, + ), + ) except Error as e: handle_fern_exception(e) raise e + def run_experiment( + self, + *, + name: str, + run_name: Optional[str] = None, + description: Optional[str] = None, + data: ExperimentData, + task: TaskFunction, + evaluators: List[EvaluatorFunction] = [], + composite_evaluator: Optional[CompositeEvaluatorFunction] = None, + run_evaluators: List[RunEvaluatorFunction] = [], + max_concurrency: int = 50, + metadata: Optional[Dict[str, str]] = None, + _dataset_version: Optional[datetime] = None, + ) -> ExperimentResult: + """Run an experiment on a dataset with automatic tracing and evaluation. + + This method executes a task function on each item in the provided dataset, + automatically traces all executions with Langfuse for observability, runs + item-level and run-level evaluators on the outputs, and returns comprehensive + results with evaluation metrics. + + The experiment system provides: + - Automatic tracing of all task executions + - Concurrent processing with configurable limits + - Comprehensive error handling that isolates failures + - Integration with Langfuse datasets for experiment tracking + - Flexible evaluation framework supporting both sync and async evaluators + + Args: + name: Human-readable name for the experiment. Used for identification + in the Langfuse UI. + run_name: Optional exact name for the experiment run. If provided, this will be + used as the exact dataset run name if the `data` contains Langfuse dataset items. + If not provided, this will default to the experiment name appended with an ISO timestamp. + description: Optional description explaining the experiment's purpose, + methodology, or expected outcomes. + data: Array of data items to process. Can be either: + - List of dict-like items with 'input', 'expected_output', 'metadata' keys + - List of Langfuse DatasetItem objects from dataset.items + task: Function that processes each data item and returns output. + Must accept 'item' as keyword argument and can return sync or async results. + The task function signature should be: task(*, item, **kwargs) -> Any + evaluators: List of functions to evaluate each item's output individually. + Each evaluator receives input, output, expected_output, and metadata. + Can return single Evaluation dict or list of Evaluation dicts. + composite_evaluator: Optional function that creates composite scores from item-level evaluations. + Receives the same inputs as item-level evaluators (input, output, expected_output, metadata) + plus the list of evaluations from item-level evaluators. Useful for weighted averages, + pass/fail decisions based on multiple criteria, or custom scoring logic combining multiple metrics. + run_evaluators: List of functions to evaluate the entire experiment run. + Each run evaluator receives all item_results and can compute aggregate metrics. + Useful for calculating averages, distributions, or cross-item comparisons. + max_concurrency: Maximum number of concurrent task executions (default: 50). + Controls the number of items processed simultaneously. Adjust based on + API rate limits and system resources. + metadata: Optional metadata dictionary to attach to all experiment traces. + This metadata will be included in every trace created during the experiment. + If `data` are Langfuse dataset items, the metadata will be attached to the dataset run, too. + + Returns: + ExperimentResult containing: + - run_name: The experiment run name. This is equal to the dataset run name if experiment was on Langfuse dataset. + - item_results: List of results for each processed item with outputs and evaluations + - run_evaluations: List of aggregate evaluation results for the entire run + - experiment_id: Stable identifier for the experiment run across all items + - dataset_run_id: ID of the dataset run (if using Langfuse datasets) + - dataset_run_url: Direct URL to view results in Langfuse UI (if applicable) + + Raises: + ValueError: If required parameters are missing or invalid + Exception: If experiment setup fails (individual item failures are handled gracefully) + + Examples: + Basic experiment with local data: + ```python + def summarize_text(*, item, **kwargs): + return f"Summary: {item['input'][:50]}..." + + def length_evaluator(*, input, output, expected_output=None, **kwargs): + return { + "name": "output_length", + "value": len(output), + "comment": f"Output contains {len(output)} characters" + } + + result = langfuse.run_experiment( + name="Text Summarization Test", + description="Evaluate summarization quality and length", + data=[ + {"input": "Long article text...", "expected_output": "Expected summary"}, + {"input": "Another article...", "expected_output": "Another summary"} + ], + task=summarize_text, + evaluators=[length_evaluator] + ) + + print(f"Processed {len(result.item_results)} items") + for item_result in result.item_results: + print(f"Input: {item_result.item['input']}") + print(f"Output: {item_result.output}") + print(f"Evaluations: {item_result.evaluations}") + ``` + + Advanced experiment with async task and multiple evaluators: + ```python + async def llm_task(*, item, **kwargs): + # Simulate async LLM call + response = await openai_client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": item["input"]}] + ) + return response.choices[0].message.content + + def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): + if expected_output and expected_output.lower() in output.lower(): + return {"name": "accuracy", "value": 1.0, "comment": "Correct answer"} + return {"name": "accuracy", "value": 0.0, "comment": "Incorrect answer"} + + def toxicity_evaluator(*, input, output, expected_output=None, **kwargs): + # Simulate toxicity check + toxicity_score = check_toxicity(output) # Your toxicity checker + return { + "name": "toxicity", + "value": toxicity_score, + "comment": f"Toxicity level: {'high' if toxicity_score > 0.7 else 'low'}" + } + + def average_accuracy(*, item_results, **kwargs): + accuracies = [ + eval.value for result in item_results + for eval in result.evaluations + if eval.name == "accuracy" + ] + return { + "name": "average_accuracy", + "value": sum(accuracies) / len(accuracies) if accuracies else 0, + "comment": f"Average accuracy across {len(accuracies)} items" + } + + result = langfuse.run_experiment( + name="LLM Safety and Accuracy Test", + description="Evaluate model accuracy and safety across diverse prompts", + data=test_dataset, # Your dataset items + task=llm_task, + evaluators=[accuracy_evaluator, toxicity_evaluator], + run_evaluators=[average_accuracy], + max_concurrency=5, # Limit concurrent API calls + metadata={"model": "gpt-4", "temperature": 0.7} + ) + ``` + + Using with Langfuse datasets: + ```python + # Get dataset from Langfuse + dataset = langfuse.get_dataset("my-eval-dataset") + + result = dataset.run_experiment( + name="Production Model Evaluation", + description="Monthly evaluation of production model performance", + task=my_production_task, + evaluators=[accuracy_evaluator, latency_evaluator] + ) + + # Results automatically linked to dataset in Langfuse UI + print(f"View results: {result['dataset_run_url']}") + ``` + + Note: + - Task and evaluator functions can be either synchronous or asynchronous + - Individual item failures are logged but don't stop the experiment + - All executions are automatically traced and visible in Langfuse UI + - When using Langfuse datasets, results are automatically linked for easy comparison + - This method works in both sync and async contexts (Jupyter notebooks, web apps, etc.) + - Async execution is handled automatically with smart event loop detection + """ + return cast( + ExperimentResult, + run_async_safely( + self._run_experiment_async( + name=name, + run_name=self._create_experiment_run_name( + name=name, run_name=run_name + ), + description=description, + data=data, + task=task, + evaluators=evaluators or [], + composite_evaluator=composite_evaluator, + run_evaluators=run_evaluators or [], + max_concurrency=max_concurrency, + metadata=metadata, + dataset_version=_dataset_version, + ), + ), + ) + + async def _run_experiment_async( + self, + *, + name: str, + run_name: str, + description: Optional[str], + data: ExperimentData, + task: TaskFunction, + evaluators: List[EvaluatorFunction], + composite_evaluator: Optional[CompositeEvaluatorFunction], + run_evaluators: List[RunEvaluatorFunction], + max_concurrency: int, + metadata: Optional[Dict[str, Any]] = None, + dataset_version: Optional[datetime] = None, + ) -> ExperimentResult: + langfuse_logger.debug( + f"Starting experiment '{name}' run '{run_name}' with {len(data)} items" + ) + + shared_fallback_experiment_id = self._create_observation_id() + + # Set up concurrency control + semaphore = asyncio.Semaphore(max_concurrency) + + # Process all items + async def process_item(item: ExperimentItem) -> ExperimentItemResult: + async with semaphore: + return await self._process_experiment_item( + item, + task, + evaluators, + composite_evaluator, + shared_fallback_experiment_id, + name, + run_name, + description, + metadata, + dataset_version, + ) + + # Run all items concurrently + tasks = [process_item(item) for item in data] + item_results = await asyncio.gather(*tasks, return_exceptions=True) + + # Filter out any exceptions and log errors + valid_results: List[ExperimentItemResult] = [] + for i, result in enumerate(item_results): + if isinstance(result, Exception): + langfuse_logger.error(f"Item {i} failed: {result}") + elif isinstance(result, ExperimentItemResult): + valid_results.append(result) # type: ignore + + # Run experiment-level evaluators + run_evaluations: List[Evaluation] = [] + for run_evaluator in run_evaluators: + try: + evaluations = await _run_evaluator( + run_evaluator, item_results=valid_results + ) + run_evaluations.extend(evaluations) + except Exception as e: + langfuse_logger.error(f"Run evaluator failed: {e}") + + # Generate dataset run URL if applicable + dataset_run_id = next( + ( + result.dataset_run_id + for result in valid_results + if result.dataset_run_id + ), + None, + ) + dataset_run_url = None + if dataset_run_id and data: + try: + # Check if the first item has dataset_id (for DatasetItem objects) + first_item = data[0] + dataset_id = None + + if hasattr(first_item, "dataset_id"): + dataset_id = getattr(first_item, "dataset_id", None) + + if dataset_id: + project_id = self._get_project_id() + + if project_id: + dataset_run_url = f"{self._base_url}/project/{project_id}/datasets/{dataset_id}/runs/{dataset_run_id}" + + except Exception: + pass # URL generation is optional + + # Store run-level evaluations as scores + for evaluation in run_evaluations: + try: + if dataset_run_id: + self.create_score( + dataset_run_id=dataset_run_id, + name=evaluation.name or "", + value=evaluation.value, # type: ignore + comment=evaluation.comment, + metadata=evaluation.metadata, + data_type=evaluation.data_type, # type: ignore + config_id=evaluation.config_id, + ) + + except Exception as e: + langfuse_logger.error(f"Failed to store run evaluation: {e}") + + # Flush scores and traces + self.flush() + + return ExperimentResult( + name=name, + run_name=run_name, + description=description, + item_results=valid_results, + run_evaluations=run_evaluations, + experiment_id=dataset_run_id or shared_fallback_experiment_id, + dataset_run_id=dataset_run_id, + dataset_run_url=dataset_run_url, + ) + + async def _process_experiment_item( + self, + item: ExperimentItem, + task: Callable, + evaluators: List[Callable], + composite_evaluator: Optional[CompositeEvaluatorFunction], + fallback_experiment_id: str, + experiment_name: str, + experiment_run_name: str, + experiment_description: Optional[str], + experiment_metadata: Optional[Dict[str, Any]] = None, + dataset_version: Optional[datetime] = None, + ) -> ExperimentItemResult: + span_name = "experiment-item-run" + + with self.start_as_current_observation(name=span_name) as span: + try: + input_data = ( + item.get("input") + if isinstance(item, dict) + else getattr(item, "input", None) + ) + + if input_data is None: + raise ValueError("Experiment Item is missing input. Skipping item.") + + expected_output = ( + item.get("expected_output") + if isinstance(item, dict) + else getattr(item, "expected_output", None) + ) + + item_metadata = ( + item.get("metadata") + if isinstance(item, dict) + else getattr(item, "metadata", None) + ) + + final_observation_metadata = { + "experiment_name": experiment_name, + "experiment_run_name": experiment_run_name, + **(experiment_metadata or {}), + } + + trace_id = span.trace_id + dataset_id = None + dataset_item_id = None + dataset_run_id = None + + # Link to dataset run if this is a dataset item + if hasattr(item, "id") and hasattr(item, "dataset_id"): + try: + # Use sync API to avoid event loop issues when run_async_safely + # creates multiple event loops across different threads + dataset_run_item = await asyncio.to_thread( + self.api.dataset_run_items.create, + run_name=experiment_run_name, + run_description=experiment_description, + metadata=experiment_metadata, + dataset_item_id=item.id, # type: ignore + trace_id=trace_id, + observation_id=span.id, + dataset_version=dataset_version, + ) + + dataset_run_id = dataset_run_item.dataset_run_id + + except Exception as e: + langfuse_logger.error(f"Failed to create dataset run item: {e}") + + if ( + not isinstance(item, dict) + and hasattr(item, "dataset_id") + and hasattr(item, "id") + ): + dataset_id = item.dataset_id + dataset_item_id = item.id + + final_observation_metadata.update( + {"dataset_id": dataset_id, "dataset_item_id": dataset_item_id} + ) + + if isinstance(item_metadata, dict): + final_observation_metadata.update(item_metadata) + + experiment_id = dataset_run_id or fallback_experiment_id + experiment_item_id = ( + dataset_item_id or get_sha256_hash_hex(_serialize(input_data))[:16] + ) + span._otel_span.set_attributes( + { + k: v + for k, v in { + LangfuseOtelSpanAttributes.ENVIRONMENT: LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + LangfuseOtelSpanAttributes.EXPERIMENT_DESCRIPTION: experiment_description, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_EXPECTED_OUTPUT: _serialize( + expected_output + ), + }.items() + if v is not None + } + ) + + propagated_experiment_attributes = PropagatedExperimentAttributes( + experiment_id=experiment_id, + experiment_name=experiment_run_name, + experiment_metadata=_flatten_and_serialize_metadata_values( + experiment_metadata + ), + experiment_dataset_id=dataset_id, + experiment_item_id=experiment_item_id, + experiment_item_metadata=_flatten_and_serialize_metadata_values( + item_metadata if isinstance(item_metadata, dict) else None + ), + experiment_item_root_observation_id=span.id, + ) + + with _propagate_attributes(experiment=propagated_experiment_attributes): + output = await _run_task(task, item) + + span.update( + input=input_data, + output=output, + metadata=final_observation_metadata, + ) + + except Exception as e: + span.update( + output=f"Error: {str(e)}", level="ERROR", status_message=str(e) + ) + raise e + + # Run evaluators + evaluations = [] + + for evaluator in evaluators: + try: + eval_metadata: Optional[Dict[str, Any]] = None + + if isinstance(item, dict): + eval_metadata = item.get("metadata") + elif hasattr(item, "metadata"): + eval_metadata = item.metadata + + with _propagate_attributes( + experiment=propagated_experiment_attributes + ): + eval_results = await _run_evaluator( + evaluator, + input=input_data, + output=output, + expected_output=expected_output, + metadata=eval_metadata, + ) + evaluations.extend(eval_results) + + # Store evaluations as scores + for evaluation in eval_results: + self.create_score( + trace_id=trace_id, + observation_id=span.id, + name=evaluation.name, + value=evaluation.value, # type: ignore + comment=evaluation.comment, + metadata=evaluation.metadata, + config_id=evaluation.config_id, + data_type=evaluation.data_type, # type: ignore + ) + + except Exception as e: + langfuse_logger.error(f"Evaluator failed: {e}") + + # Run composite evaluator if provided and we have evaluations + if composite_evaluator and evaluations: + try: + composite_eval_metadata: Optional[Dict[str, Any]] = None + if isinstance(item, dict): + composite_eval_metadata = item.get("metadata") + elif hasattr(item, "metadata"): + composite_eval_metadata = item.metadata + + with _propagate_attributes( + experiment=propagated_experiment_attributes + ): + result = composite_evaluator( + input=input_data, + output=output, + expected_output=expected_output, + metadata=composite_eval_metadata, + evaluations=evaluations, + ) + + # Handle async composite evaluators + if asyncio.iscoroutine(result): + result = await result + + # Normalize to list + composite_evals: List[Evaluation] = [] + if isinstance(result, (dict, Evaluation)): + composite_evals = [result] # type: ignore + elif isinstance(result, list): + composite_evals = result # type: ignore + + # Store composite evaluations as scores and add to evaluations list + for composite_evaluation in composite_evals: + self.create_score( + trace_id=trace_id, + observation_id=span.id, + name=composite_evaluation.name, + value=composite_evaluation.value, # type: ignore + comment=composite_evaluation.comment, + metadata=composite_evaluation.metadata, + config_id=composite_evaluation.config_id, + data_type=composite_evaluation.data_type, # type: ignore + ) + evaluations.append(composite_evaluation) + + except Exception as e: + langfuse_logger.error(f"Composite evaluator failed: {e}") + + return ExperimentItemResult( + item=item, + output=output, + evaluations=evaluations, + trace_id=trace_id, + dataset_run_id=dataset_run_id, + ) + + def _create_experiment_run_name( + self, *, name: Optional[str] = None, run_name: Optional[str] = None + ) -> str: + if run_name: + return run_name + + iso_timestamp = _get_timestamp().isoformat().replace("+00:00", "Z") + + return f"{name} - {iso_timestamp}" + + def run_batched_evaluation( + self, + *, + scope: Literal["traces", "observations"], + mapper: MapperFunction, + filter: Optional[str] = None, + fetch_batch_size: int = 50, + fetch_trace_fields: Optional[str] = None, + max_items: Optional[int] = None, + max_retries: int = 3, + evaluators: List[EvaluatorFunction], + composite_evaluator: Optional[CompositeEvaluatorFunction] = None, + max_concurrency: int = 5, + metadata: Optional[Dict[str, Any]] = None, + _add_observation_scores_to_trace: bool = False, + _additional_trace_tags: Optional[List[str]] = None, + resume_from: Optional[BatchEvaluationResumeToken] = None, + verbose: bool = False, + ) -> BatchEvaluationResult: + """Fetch traces or observations and run evaluations on each item. + + This method provides a powerful way to evaluate existing data in Langfuse at scale. + It fetches items based on filters, transforms them using a mapper function, runs + evaluators on each item, and creates scores that are linked back to the original + entities. This is ideal for: + + - Running evaluations on production traces after deployment + - Backtesting new evaluation metrics on historical data + - Batch scoring of observations for quality monitoring + - Periodic evaluation runs on recent data + + The method uses a streaming/pipeline approach to process items in batches, making + it memory-efficient for large datasets. It includes comprehensive error handling, + retry logic, and resume capability for long-running evaluations. + + Args: + scope: The type of items to evaluate. Must be one of: + - "traces": Evaluate complete traces with all their observations + - "observations": Evaluate individual observations (spans, generations, events) + mapper: Function that transforms API response objects into evaluator inputs. + Receives a trace/observation object and returns an EvaluatorInputs + instance with input, output, expected_output, and metadata fields. + Can be sync or async. + evaluators: List of evaluation functions to run on each item. Each evaluator + receives the mapped inputs and returns Evaluation object(s). Evaluator + failures are logged but don't stop the batch evaluation. + filter: Optional JSON filter string for querying items (same format as Langfuse API). Examples: + - '{"tags": ["production"]}' + - '{"user_id": "user123", "timestamp": {"operator": ">", "value": "2024-01-01"}}' + Default: None (fetches all items). + fetch_batch_size: Number of items to fetch per API call and hold in memory. + Larger values may be faster but use more memory. Default: 50. + fetch_trace_fields: Comma-separated list of fields to include when fetching traces. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. Only relevant if scope is 'traces'. + max_items: Maximum total number of items to process. If None, processes all + items matching the filter. Useful for testing or limiting evaluation runs. + Default: None (process all). + max_concurrency: Maximum number of items to evaluate concurrently. Controls + parallelism and resource usage. Default: 5. + composite_evaluator: Optional function that creates a composite score from + item-level evaluations. Receives the original item and its evaluations, + returns a single Evaluation. Useful for weighted averages or combined metrics. + Default: None. + metadata: Optional metadata dict to add to all created scores. Useful for + tracking evaluation runs, versions, or other context. Default: None. + max_retries: Maximum number of retry attempts for failed batch fetches. + Uses exponential backoff (1s, 2s, 4s). Default: 3. + verbose: If True, logs progress information to console. Useful for monitoring + long-running evaluations. Default: False. + resume_from: Optional resume token from a previous incomplete run. Allows + continuing evaluation after interruption or failure. Default: None. + + + Returns: + BatchEvaluationResult containing: + - total_items_fetched: Number of items fetched from API + - total_items_processed: Number of items successfully evaluated + - total_items_failed: Number of items that failed evaluation + - total_scores_created: Scores created by item-level evaluators + - total_composite_scores_created: Scores created by composite evaluator + - total_evaluations_failed: Individual evaluator failures + - evaluator_stats: Per-evaluator statistics (success rate, scores created) + - resume_token: Token for resuming if incomplete (None if completed) + - completed: True if all items processed + - duration_seconds: Total execution time + - failed_item_ids: IDs of items that failed + - error_summary: Error types and counts + - has_more_items: True if max_items reached but more exist + + Raises: + ValueError: If invalid scope is provided. + + Examples: + Basic trace evaluation: + ```python + from langfuse import Langfuse, EvaluatorInputs, Evaluation + + client = Langfuse() + + # Define mapper to extract fields from traces + def trace_mapper(trace): + return EvaluatorInputs( + input=trace.input, + output=trace.output, + expected_output=None, + metadata={"trace_id": trace.id} + ) + + # Define evaluator + def length_evaluator(*, input, output, expected_output, metadata): + return Evaluation( + name="output_length", + value=len(output) if output else 0 + ) + + # Run batch evaluation + result = client.run_batched_evaluation( + scope="traces", + mapper=trace_mapper, + evaluators=[length_evaluator], + filter='{"tags": ["production"]}', + max_items=1000, + verbose=True + ) + + print(f"Processed {result.total_items_processed} traces") + print(f"Created {result.total_scores_created} scores") + ``` + + Evaluation with composite scorer: + ```python + def accuracy_evaluator(*, input, output, expected_output, metadata): + # ... evaluation logic + return Evaluation(name="accuracy", value=0.85) + + def relevance_evaluator(*, input, output, expected_output, metadata): + # ... evaluation logic + return Evaluation(name="relevance", value=0.92) + + def composite_evaluator(*, item, evaluations): + # Weighted average of evaluations + weights = {"accuracy": 0.6, "relevance": 0.4} + total = sum( + e.value * weights.get(e.name, 0) + for e in evaluations + if isinstance(e.value, (int, float)) + ) + return Evaluation( + name="composite_score", + value=total, + comment=f"Weighted average of {len(evaluations)} metrics" + ) + + result = client.run_batched_evaluation( + scope="traces", + mapper=trace_mapper, + evaluators=[accuracy_evaluator, relevance_evaluator], + composite_evaluator=composite_evaluator, + filter='{"user_id": "important_user"}', + verbose=True + ) + ``` + + Handling incomplete runs with resume: + ```python + # Initial run that may fail or timeout + result = client.run_batched_evaluation( + scope="observations", + mapper=obs_mapper, + evaluators=[my_evaluator], + max_items=10000, + verbose=True + ) + + # Check if incomplete + if not result.completed and result.resume_token: + print(f"Processed {result.resume_token.items_processed} items before interruption") + + # Resume from where it left off + result = client.run_batched_evaluation( + scope="observations", + mapper=obs_mapper, + evaluators=[my_evaluator], + resume_from=result.resume_token, + verbose=True + ) + + print(f"Total items processed: {result.total_items_processed}") + ``` + + Monitoring evaluator performance: + ```python + result = client.run_batched_evaluation(...) + + for stats in result.evaluator_stats: + success_rate = stats.successful_runs / stats.total_runs + print(f"{stats.name}:") + print(f" Success rate: {success_rate:.1%}") + print(f" Scores created: {stats.total_scores_created}") + + if stats.failed_runs > 0: + print(f" ⚠️ Failed {stats.failed_runs} times") + ``` + + Note: + - Evaluator failures are logged but don't stop the batch evaluation + - Individual item failures are tracked but don't stop processing + - Fetch failures are retried with exponential backoff + - All scores are automatically flushed to Langfuse at the end + - The resume mechanism uses timestamp-based filtering to avoid duplicates + """ + runner = BatchEvaluationRunner(self) + + return cast( + BatchEvaluationResult, + run_async_safely( + runner.run_async( + scope=scope, + mapper=mapper, + evaluators=evaluators, + filter=filter, + fetch_batch_size=fetch_batch_size, + fetch_trace_fields=fetch_trace_fields, + max_items=max_items, + max_concurrency=max_concurrency, + composite_evaluator=composite_evaluator, + metadata=metadata, + _add_observation_scores_to_trace=_add_observation_scores_to_trace, + _additional_trace_tags=_additional_trace_tags, + max_retries=max_retries, + verbose=verbose, + resume_from=resume_from, + ) + ), + ) + def auth_check(self) -> bool: """Check if the provided credentials (public and secret key) are valid. @@ -1804,6 +3216,8 @@ def create_dataset( name: str, description: Optional[str] = None, metadata: Optional[Any] = None, + input_schema: Optional[Any] = None, + expected_output_schema: Optional[Any] = None, ) -> Dataset: """Create a dataset with the given name on Langfuse. @@ -1811,17 +3225,24 @@ def create_dataset( name: Name of the dataset to create. description: Description of the dataset. Defaults to None. metadata: Additional metadata. Defaults to None. + input_schema: JSON Schema for validating dataset item inputs. When set, all new items will be validated against this schema. + expected_output_schema: JSON Schema for validating dataset item expected outputs. When set, all new items will be validated against this schema. Returns: Dataset: The created dataset as returned by the Langfuse API. """ try: - body = CreateDatasetRequest( - name=name, description=description, metadata=metadata + langfuse_logger.debug(f"Creating datasets {name}") + + result = self.api.datasets.create( + name=name, + description=description, + metadata=metadata, + input_schema=input_schema, + expected_output_schema=expected_output_schema, ) - langfuse_logger.debug(f"Creating datasets {body}") - return self.api.datasets.create(request=body) + return cast(Dataset, result) except Error as e: handle_fern_exception(e) @@ -1872,18 +3293,20 @@ def create_dataset_item( ``` """ try: - body = CreateDatasetItemRequest( - datasetName=dataset_name, + langfuse_logger.debug(f"Creating dataset item for dataset {dataset_name}") + + result = self.api.dataset_items.create( + dataset_name=dataset_name, input=input, - expectedOutput=expected_output, + expected_output=expected_output, metadata=metadata, - sourceTraceId=source_trace_id, - sourceObservationId=source_observation_id, + source_trace_id=source_trace_id, + source_observation_id=source_observation_id, status=status, id=id, ) - langfuse_logger.debug(f"Creating dataset item {body}") - return self.api.dataset_items.create(request=body) + + return cast(DatasetItem, result) except Error as e: handle_fern_exception(e) raise e @@ -2014,7 +3437,7 @@ def get_prompt( """ if self._resources is None: raise Error( - "SDK is not correctly initalized. Check the init logs for more details." + "SDK is not correctly initialized. Check the init logs for more details." ) if version is not None and label is not None: raise ValueError("Cannot specify both version and label at the same time.") @@ -2089,8 +3512,9 @@ def refresh_task() -> None: fetch_timeout_seconds=fetch_timeout_seconds, ) - self._resources.prompt_cache.add_refresh_prompt_task( + self._resources.prompt_cache.add_refresh_prompt_task_if_current( cache_key, + cached_prompt, refresh_task, ) langfuse_logger.debug( @@ -2151,6 +3575,14 @@ def fetch_prompts() -> Any: return prompt + except NotFoundError as not_found_error: + langfuse_logger.warning( + f"Prompt '{cache_key}' not found during refresh, evicting from cache." + ) + if self._resources is not None: + self._resources.prompt_cache.delete(cache_key) + raise not_found_error + except Exception as e: langfuse_logger.error( f"Error while fetching prompt '{cache_key}': {str(e)}" @@ -2237,15 +3669,15 @@ def create_prompt( raise ValueError( "For 'chat' type, 'prompt' must be a list of chat messages with role and content attributes." ) - request: Union[CreatePromptRequest_Chat, CreatePromptRequest_Text] = ( - CreatePromptRequest_Chat( + request: Union[CreateChatPromptRequest, CreateTextPromptRequest] = ( + CreateChatPromptRequest( name=name, prompt=cast(Any, prompt), labels=labels, tags=tags, config=config or {}, - commitMessage=commit_message, - type="chat", + commit_message=commit_message, + type=CreateChatPromptType.CHAT, ) ) server_prompt = self.api.prompts.create(request=request) @@ -2258,14 +3690,13 @@ def create_prompt( if not isinstance(prompt, str): raise ValueError("For 'text' type, 'prompt' must be a string.") - request = CreatePromptRequest_Text( + request = CreateTextPromptRequest( name=name, prompt=prompt, labels=labels, tags=tags, config=config or {}, - commitMessage=commit_message, - type="text", + commit_message=commit_message, ) server_prompt = self.api.prompts.create(request=request) @@ -2298,7 +3729,7 @@ def update_prompt( """ updated_prompt = self.api.prompt_version.update( - name=name, + name=self._url_encode(name), version=version, new_labels=new_labels, ) @@ -2320,3 +3751,13 @@ def _url_encode(self, url: str, *, is_url_param: Optional[bool] = False) -> str: # we need add safe="" to force escaping of slashes # This is necessary for prompts in prompt folders return urllib.parse.quote(url, safe="") + + def clear_prompt_cache(self) -> None: + """Clear the entire prompt cache, removing all cached prompts. + + This method is useful when you want to force a complete refresh of all + cached prompts, for example after major updates or when you need to + ensure the latest versions are fetched from the server. + """ + if self._resources is not None: + self._resources.prompt_cache.clear() diff --git a/langfuse/_client/constants.py b/langfuse/_client/constants.py index 1c805ddc3..c2d0aa7aa 100644 --- a/langfuse/_client/constants.py +++ b/langfuse/_client/constants.py @@ -3,4 +3,63 @@ This module defines constants used throughout the Langfuse OpenTelemetry integration. """ +from typing import Any, List, Literal, Union, get_args + +from typing_extensions import TypeAlias + LANGFUSE_TRACER_NAME = "langfuse-sdk" + +LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT = "sdk-experiment" + +"""Note: this type is used with .__args__ / get_args in some cases and therefore must remain flat""" +ObservationTypeGenerationLike: TypeAlias = Literal[ + "generation", + "embedding", +] + +ObservationTypeSpanLike: TypeAlias = Literal[ + "span", + "agent", + "tool", + "chain", + "retriever", + "evaluator", + "guardrail", +] + +ObservationTypeLiteralNoEvent: TypeAlias = Union[ + ObservationTypeGenerationLike, + ObservationTypeSpanLike, +] + +"""Enumeration of valid observation types for Langfuse tracing. + +This Literal defines all available observation types that can be used with the @observe +decorator and other Langfuse SDK methods. +""" +ObservationTypeLiteral: TypeAlias = Union[ + ObservationTypeLiteralNoEvent, Literal["event"] +] + + +def get_observation_types_list( + literal_type: Any, +) -> List[str]: + """Flattens the Literal type to provide a list of strings. + + Args: + literal_type: A Literal type, TypeAlias, or union of Literals to flatten + + Returns: + Flat list of all string values contained in the Literal type + """ + result = [] + args = get_args(literal_type) + + for arg in args: + if hasattr(arg, "__args__"): + result.extend(get_observation_types_list(arg)) + else: + result.append(arg) + + return result diff --git a/langfuse/_client/datasets.py b/langfuse/_client/datasets.py index f06570e57..e28ddbebf 100644 --- a/langfuse/_client/datasets.py +++ b/langfuse/_client/datasets.py @@ -1,140 +1,22 @@ import datetime as dt -import logging -from .span import LangfuseSpan -from typing import TYPE_CHECKING, Any, Generator, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional -from opentelemetry.util._decorator import _agnosticcontextmanager - -from langfuse.model import ( - CreateDatasetRunItemRequest, +from langfuse.api import ( Dataset, DatasetItem, - DatasetStatus, +) +from langfuse.batch_evaluation import CompositeEvaluatorFunction +from langfuse.experiment import ( + EvaluatorFunction, + ExperimentResult, + RunEvaluatorFunction, + TaskFunction, ) if TYPE_CHECKING: from langfuse._client.client import Langfuse -class DatasetItemClient: - """Class for managing dataset items in Langfuse. - - Args: - id (str): Unique identifier of the dataset item. - status (DatasetStatus): The status of the dataset item. Can be either 'ACTIVE' or 'ARCHIVED'. - input (Any): Input data of the dataset item. - expected_output (Optional[Any]): Expected output of the dataset item. - metadata (Optional[Any]): Additional metadata of the dataset item. - source_trace_id (Optional[str]): Identifier of the source trace. - source_observation_id (Optional[str]): Identifier of the source observation. - dataset_id (str): Identifier of the dataset to which this item belongs. - dataset_name (str): Name of the dataset to which this item belongs. - created_at (datetime): Timestamp of dataset item creation. - updated_at (datetime): Timestamp of the last update to the dataset item. - langfuse (Langfuse): Instance of Langfuse client for API interactions. - - Example: - ```python - from langfuse import Langfuse - - langfuse = Langfuse() - - dataset = langfuse.get_dataset("") - - for item in dataset.items: - # Generate a completion using the input of every item - completion, generation = llm_app.run(item.input) - - # Evaluate the completion - generation.score( - name="example-score", - value=1 - ) - ``` - """ - - log = logging.getLogger("langfuse") - - id: str - status: DatasetStatus - input: Any - expected_output: Optional[Any] - metadata: Optional[Any] - source_trace_id: Optional[str] - source_observation_id: Optional[str] - dataset_id: str - dataset_name: str - created_at: dt.datetime - updated_at: dt.datetime - - langfuse: "Langfuse" - - def __init__(self, dataset_item: DatasetItem, langfuse: "Langfuse"): - """Initialize the DatasetItemClient.""" - self.id = dataset_item.id - self.status = dataset_item.status - self.input = dataset_item.input - self.expected_output = dataset_item.expected_output - self.metadata = dataset_item.metadata - self.source_trace_id = dataset_item.source_trace_id - self.source_observation_id = dataset_item.source_observation_id - self.dataset_id = dataset_item.dataset_id - self.dataset_name = dataset_item.dataset_name - self.created_at = dataset_item.created_at - self.updated_at = dataset_item.updated_at - - self.langfuse = langfuse - - @_agnosticcontextmanager - def run( - self, - *, - run_name: str, - run_metadata: Optional[Any] = None, - run_description: Optional[str] = None, - ) -> Generator[LangfuseSpan, None, None]: - """Create a context manager for the dataset item run that links the execution to a Langfuse trace. - - This method is a context manager that creates a trace for the dataset run and yields a span - that can be used to track the execution of the run. - - Args: - run_name (str): The name of the dataset run. - run_metadata (Optional[Any]): Additional metadata to include in dataset run. - run_description (Optional[str]): Description of the dataset run. - - Yields: - span: A LangfuseSpan that can be used to trace the execution of the run. - """ - trace_name = f"Dataset run: {run_name}" - - with self.langfuse.start_as_current_span(name=trace_name) as span: - span.update_trace( - name=trace_name, - metadata={ - "dataset_item_id": self.id, - "run_name": run_name, - "dataset_id": self.dataset_id, - }, - ) - - self.log.debug( - f"Creating dataset run item: run_name={run_name} id={self.id} trace_id={span.trace_id}" - ) - - self.langfuse.api.dataset_run_items.create( - request=CreateDatasetRunItemRequest( - runName=run_name, - datasetItemId=self.id, - traceId=span.trace_id, - metadata=run_metadata, - runDescription=run_description, - ) - ) - - yield span - - class DatasetClient: """Class for managing datasets in Langfuse. @@ -146,8 +28,8 @@ class DatasetClient: project_id (str): Identifier of the project to which the dataset belongs. created_at (datetime): Timestamp of dataset creation. updated_at (datetime): Timestamp of the last update to the dataset. - items (List[DatasetItemClient]): List of dataset items associated with the dataset. - + items (List[DatasetItem]): List of dataset items associated with the dataset. + version (Optional[datetime]): Timestamp of the dataset version. Example: Print the input of each dataset item in a dataset. ```python @@ -169,9 +51,19 @@ class DatasetClient: metadata: Optional[Any] created_at: dt.datetime updated_at: dt.datetime - items: List[DatasetItemClient] + input_schema: Optional[Any] + expected_output_schema: Optional[Any] + items: List[DatasetItem] + version: Optional[dt.datetime] - def __init__(self, dataset: Dataset, items: List[DatasetItemClient]): + def __init__( + self, + *, + dataset: Dataset, + items: List[DatasetItem], + langfuse_client: "Langfuse", + version: Optional[dt.datetime] = None, + ): """Initialize the DatasetClient.""" self.id = dataset.id self.name = dataset.name @@ -180,4 +72,231 @@ def __init__(self, dataset: Dataset, items: List[DatasetItemClient]): self.metadata = dataset.metadata self.created_at = dataset.created_at self.updated_at = dataset.updated_at + self.input_schema = dataset.input_schema + self.expected_output_schema = dataset.expected_output_schema self.items = items + self.version = version + self._langfuse_client: "Langfuse" = langfuse_client + + def run_experiment( + self, + *, + name: str, + run_name: Optional[str] = None, + description: Optional[str] = None, + task: TaskFunction, + evaluators: List[EvaluatorFunction] = [], + composite_evaluator: Optional[CompositeEvaluatorFunction] = None, + run_evaluators: List[RunEvaluatorFunction] = [], + max_concurrency: int = 50, + metadata: Optional[Dict[str, Any]] = None, + ) -> ExperimentResult: + """Run an experiment on this Langfuse dataset with automatic tracking. + + This is a convenience method that runs an experiment using all items in this + dataset. It automatically creates a dataset run in Langfuse for tracking and + comparison purposes, linking all experiment results to the dataset. + + Key benefits of using dataset.run_experiment(): + - Automatic dataset run creation and linking in Langfuse UI + - Built-in experiment tracking and versioning + - Easy comparison between different experiment runs + - Direct access to dataset items with their metadata and expected outputs + - Automatic URL generation for viewing results in Langfuse dashboard + + Args: + name: Human-readable name for the experiment run. This will be used as + the dataset run name in Langfuse for tracking and identification. + run_name: Optional exact name for the dataset run. If provided, this will be + used as the exact dataset run name in Langfuse. If not provided, this will + default to the experiment name appended with an ISO timestamp. + description: Optional description of the experiment's purpose, methodology, + or what you're testing. Appears in the Langfuse UI for context. + task: Function that processes each dataset item and returns output. + The function will receive DatasetItem objects with .input, .expected_output, + .metadata attributes. Signature should be: task(*, item, **kwargs) -> Any + evaluators: List of functions to evaluate each item's output individually. + These will have access to the item's expected_output for comparison. + composite_evaluator: Optional function that creates composite scores from item-level evaluations. + Receives the same inputs as item-level evaluators (input, output, expected_output, metadata) + plus the list of evaluations from item-level evaluators. Useful for weighted averages, + pass/fail decisions based on multiple criteria, or custom scoring logic combining multiple metrics. + run_evaluators: List of functions to evaluate the entire experiment run. + Useful for computing aggregate statistics across all dataset items. + max_concurrency: Maximum number of concurrent task executions (default: 50). + Adjust based on API rate limits and system resources. + metadata: Optional metadata to attach to the experiment run and all traces. + Will be combined with individual item metadata. + + Returns: + ExperimentResult object containing: + - name: The experiment name. + - run_name: The experiment run name (equivalent to the dataset run name). + - description: Optional experiment description. + - item_results: Results for each dataset item with outputs and evaluations. + - run_evaluations: Aggregate evaluation results for the entire run. + - dataset_run_id: ID of the created dataset run in Langfuse. + - dataset_run_url: Direct URL to view the experiment results in Langfuse UI. + + The result object provides a format() method for human-readable output: + ```python + result = dataset.run_experiment(...) + print(result.format()) # Summary view + print(result.format(include_item_results=True)) # Detailed view + ``` + + Raises: + ValueError: If the dataset has no items or no Langfuse client is available. + + Examples: + Basic dataset experiment: + ```python + dataset = langfuse.get_dataset("qa-evaluation-set") + + def answer_questions(*, item, **kwargs): + # item is a DatasetItem with .input, .expected_output, .metadata + question = item.input + return my_qa_system.answer(question) + + def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): + if not expected_output: + return {"name": "accuracy", "value": 0, "comment": "No expected output"} + + is_correct = output.strip().lower() == expected_output.strip().lower() + return { + "name": "accuracy", + "value": 1.0 if is_correct else 0.0, + "comment": "Correct" if is_correct else "Incorrect" + } + + result = dataset.run_experiment( + name="QA System v2.0 Evaluation", + description="Testing improved QA system on curated question set", + task=answer_questions, + evaluators=[accuracy_evaluator] + ) + + print(f"Evaluated {len(result['item_results'])} questions") + print(f"View detailed results: {result['dataset_run_url']}") + ``` + + Advanced experiment with multiple evaluators and run-level analysis: + ```python + dataset = langfuse.get_dataset("content-generation-benchmark") + + async def generate_content(*, item, **kwargs): + prompt = item.input + response = await openai_client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": prompt}], + temperature=0.7 + ) + return response.choices[0].message.content + + def quality_evaluator(*, input, output, expected_output=None, metadata=None, **kwargs): + # Use metadata for context-aware evaluation + content_type = metadata.get("type", "general") if metadata else "general" + + # Basic quality checks + word_count = len(output.split()) + min_words = {"blog": 300, "tweet": 10, "summary": 100}.get(content_type, 50) + + return [ + { + "name": "word_count", + "value": word_count, + "comment": f"Generated {word_count} words" + }, + { + "name": "meets_length_requirement", + "value": word_count >= min_words, + "comment": f"{'Meets' if word_count >= min_words else 'Below'} minimum {min_words} words for {content_type}" + } + ] + + def content_diversity(*, item_results, **kwargs): + # Analyze diversity across all generated content + all_outputs = [result["output"] for result in item_results] + unique_words = set() + total_words = 0 + + for output in all_outputs: + words = output.lower().split() + unique_words.update(words) + total_words += len(words) + + diversity_ratio = len(unique_words) / total_words if total_words > 0 else 0 + + return { + "name": "vocabulary_diversity", + "value": diversity_ratio, + "comment": f"Used {len(unique_words)} unique words out of {total_words} total ({diversity_ratio:.2%} diversity)" + } + + result = dataset.run_experiment( + name="Content Generation Diversity Test", + description="Evaluating content quality and vocabulary diversity across different content types", + task=generate_content, + evaluators=[quality_evaluator], + run_evaluators=[content_diversity], + max_concurrency=3, # Limit API calls + metadata={"model": "gpt-4", "temperature": 0.7} + ) + + # Results are automatically linked to dataset in Langfuse + print(f"Experiment completed! View in Langfuse: {result['dataset_run_url']}") + + # Access individual results + for i, item_result in enumerate(result["item_results"]): + print(f"Item {i+1}: {item_result['evaluations']}") + ``` + + Comparing different model versions: + ```python + # Run multiple experiments on the same dataset for comparison + dataset = langfuse.get_dataset("model-benchmark") + + # Experiment 1: GPT-4 + result_gpt4 = dataset.run_experiment( + name="GPT-4 Baseline", + description="Baseline performance with GPT-4", + task=lambda *, item, **kwargs: gpt4_model.generate(item.input), + evaluators=[accuracy_evaluator, fluency_evaluator] + ) + + # Experiment 2: Custom model + result_custom = dataset.run_experiment( + name="Custom Model v1.2", + description="Testing our fine-tuned model", + task=lambda *, item, **kwargs: custom_model.generate(item.input), + evaluators=[accuracy_evaluator, fluency_evaluator] + ) + + # Both experiments are now visible in Langfuse for easy comparison + print("Compare results in Langfuse:") + print(f"GPT-4: {result_gpt4.dataset_run_url}") + print(f"Custom: {result_custom.dataset_run_url}") + ``` + + Note: + - All experiment results are automatically tracked in Langfuse as dataset runs + - Dataset items provide .input, .expected_output, and .metadata attributes + - Results can be easily compared across different experiment runs in the UI + - The dataset_run_url provides direct access to detailed results and analysis + - Failed items are handled gracefully and logged without stopping the experiment + - This method works in both sync and async contexts (Jupyter notebooks, web apps, etc.) + - Async execution is handled automatically with smart event loop detection + """ + return self._langfuse_client.run_experiment( + name=name, + run_name=run_name, + description=description, + data=self.items, + task=task, + evaluators=evaluators, + composite_evaluator=composite_evaluator, + run_evaluators=run_evaluators, + max_concurrency=max_concurrency, + metadata=metadata, + _dataset_version=self.version, + ) diff --git a/langfuse/_client/environment_variables.py b/langfuse/_client/environment_variables.py index b868b1e24..6b421578a 100644 --- a/langfuse/_client/environment_variables.py +++ b/langfuse/_client/environment_variables.py @@ -35,15 +35,33 @@ Secret API key of Langfuse project """ +LANGFUSE_BASE_URL = "LANGFUSE_BASE_URL" +""" +.. envvar:: LANGFUSE_BASE_URL + +Base URL of Langfuse API. Can be set via `LANGFUSE_BASE_URL` environment variable. + +**Default value:** ``"https://cloud.langfuse.com"`` +""" + LANGFUSE_HOST = "LANGFUSE_HOST" """ .. envvar:: LANGFUSE_HOST -Host of Langfuse API. Can be set via `LANGFUSE_HOST` environment variable. +Deprecated. Use LANGFUSE_BASE_URL instead. Host of Langfuse API. Can be set via `LANGFUSE_HOST` environment variable. **Default value:** ``"https://cloud.langfuse.com"`` """ +LANGFUSE_OTEL_TRACES_EXPORT_PATH = "LANGFUSE_OTEL_TRACES_EXPORT_PATH" +""" +.. envvar:: LANGFUSE_OTEL_TRACES_EXPORT_PATH + +URL path on the configured host to export traces to. + +**Default value:** ``/api/public/otel/v1/traces`` +""" + LANGFUSE_DEBUG = "LANGFUSE_DEBUG" """ .. envvar:: LANGFUSE_DEBUG @@ -76,7 +94,7 @@ .. envvar:: LANGFUSE_FLUSH_AT Max batch size until a new ingestion batch is sent to the API. -**Default value:** ``15`` +**Default value:** same as OTEL ``OTEL_BSP_MAX_EXPORT_BATCH_SIZE`` """ LANGFUSE_FLUSH_INTERVAL = "LANGFUSE_FLUSH_INTERVAL" @@ -84,7 +102,7 @@ .. envvar:: LANGFUSE_FLUSH_INTERVAL Max delay in seconds until a new ingestion batch is sent to the API. -**Default value:** ``1`` +**Default value:** same as OTEL ``OTEL_BSP_SCHEDULE_DELAY`` """ LANGFUSE_SAMPLE_RATE = "LANGFUSE_SAMPLE_RATE" @@ -128,3 +146,13 @@ **Default value**: ``5`` """ + +LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS = "LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS" +""" +.. envvar: LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS + +Controls the default time-to-live (TTL) in seconds for cached prompts. +This setting determines how long prompt responses are cached before they expire. + +**Default value**: ``60`` +""" diff --git a/langfuse/_client/get_client.py b/langfuse/_client/get_client.py index 98a64fbfe..e2ebab0ed 100644 --- a/langfuse/_client/get_client.py +++ b/langfuse/_client/get_client.py @@ -1,9 +1,63 @@ -from typing import Optional +from contextlib import contextmanager +from contextvars import ContextVar +from typing import Iterator, Optional from langfuse._client.client import Langfuse from langfuse._client.resource_manager import LangfuseResourceManager from langfuse.logger import langfuse_logger +# Context variable to track the current langfuse_public_key in execution context +_current_public_key: ContextVar[Optional[str]] = ContextVar( + "langfuse_public_key", default=None +) + + +@contextmanager +def _set_current_public_key(public_key: Optional[str]) -> Iterator[None]: + """Context manager to set and restore the current public key in execution context. + + Args: + public_key: The public key to set in context. If None, context is not modified. + + Yields: + None + """ + if public_key is None: + yield # Don't modify context if no key provided + return + + token = _current_public_key.set(public_key) + try: + yield + finally: + _current_public_key.reset(token) + + +def _create_client_from_instance( + instance: "LangfuseResourceManager", public_key: Optional[str] = None +) -> Langfuse: + """Create a Langfuse client from a resource manager instance with all settings preserved.""" + return Langfuse( + public_key=public_key or instance.public_key, + secret_key=instance.secret_key, + base_url=instance.base_url, + tracing_enabled=instance.tracing_enabled, + environment=instance.environment, + timeout=instance.timeout, + flush_at=instance.flush_at, + flush_interval=instance.flush_interval, + release=instance.release, + media_upload_thread_count=instance.media_upload_thread_count, + sample_rate=instance.sample_rate, + mask=instance.mask, + blocked_instrumentation_scopes=instance.blocked_instrumentation_scopes, + should_export_span=instance.should_export_span, + additional_headers=instance.additional_headers, + tracer_provider=instance.tracer_provider, + span_exporter=instance.span_exporter, + httpx_client=instance.httpx_client, + ) + def get_client(*, public_key: Optional[str] = None) -> Langfuse: """Get or create a Langfuse client instance. @@ -49,6 +103,10 @@ def get_client(*, public_key: Optional[str] = None) -> Langfuse: with LangfuseResourceManager._lock: active_instances = LangfuseResourceManager._instances + # If no explicit public_key provided, check execution context + if not public_key: + public_key = _current_public_key.get(None) + if not public_key: if len(active_instances) == 0: # No clients initialized yet, create default instance @@ -61,12 +119,7 @@ def get_client(*, public_key: Optional[str] = None) -> Langfuse: # Initialize with the credentials bound to the instance # This is important if the original instance was instantiated # via constructor arguments - return Langfuse( - public_key=instance.public_key, - secret_key=instance.secret_key, - host=instance.host, - tracing_enabled=instance.tracing_enabled, - ) + return _create_client_from_instance(instance) else: # Multiple clients exist but no key specified - disable tracing @@ -94,9 +147,4 @@ def get_client(*, public_key: Optional[str] = None) -> Langfuse: ) # target_instance is guaranteed to be not None at this point - return Langfuse( - public_key=public_key, - secret_key=target_instance.secret_key, - host=target_instance.host, - tracing_enabled=target_instance.tracing_enabled, - ) + return _create_client_from_instance(target_instance, public_key) diff --git a/langfuse/_client/observe.py b/langfuse/_client/observe.py index 0e68bc965..64882a20f 100644 --- a/langfuse/_client/observe.py +++ b/langfuse/_client/observe.py @@ -1,6 +1,6 @@ import asyncio +import contextvars import inspect -import logging import os from functools import wraps from typing import ( @@ -10,7 +10,7 @@ Dict, Generator, Iterable, - Literal, + List, Optional, Tuple, TypeVar, @@ -22,11 +22,26 @@ from opentelemetry.util._decorator import _AgnosticContextManager from typing_extensions import ParamSpec +from langfuse._client.constants import ( + ObservationTypeLiteralNoEvent, + get_observation_types_list, +) from langfuse._client.environment_variables import ( LANGFUSE_OBSERVE_DECORATOR_IO_CAPTURE_ENABLED, ) -from langfuse._client.get_client import get_client -from langfuse._client.span import LangfuseGeneration, LangfuseSpan +from langfuse._client.get_client import _set_current_public_key, get_client +from langfuse._client.span import ( + LangfuseAgent, + LangfuseChain, + LangfuseEmbedding, + LangfuseEvaluator, + LangfuseGeneration, + LangfuseGuardrail, + LangfuseRetriever, + LangfuseSpan, + LangfuseTool, +) +from langfuse.logger import langfuse_logger as logger from langfuse.types import TraceContext F = TypeVar("F", bound=Callable[..., Any]) @@ -54,8 +69,6 @@ class LangfuseDecorator: - Thread-safe client resolution when multiple Langfuse projects are used """ - _log = logging.getLogger("langfuse") - @overload def observe(self, func: F) -> F: ... @@ -65,7 +78,7 @@ def observe( func: None = None, *, name: Optional[str] = None, - as_type: Optional[Literal["generation"]] = None, + as_type: Optional[ObservationTypeLiteralNoEvent] = None, capture_input: Optional[bool] = None, capture_output: Optional[bool] = None, transform_to_string: Optional[Callable[[Iterable], str]] = None, @@ -76,7 +89,7 @@ def observe( func: Optional[F] = None, *, name: Optional[str] = None, - as_type: Optional[Literal["generation"]] = None, + as_type: Optional[ObservationTypeLiteralNoEvent] = None, capture_input: Optional[bool] = None, capture_output: Optional[bool] = None, transform_to_string: Optional[Callable[[Iterable], str]] = None, @@ -93,8 +106,11 @@ def observe( Args: func (Optional[Callable]): The function to decorate. When used with parentheses @observe(), this will be None. name (Optional[str]): Custom name for the created trace or span. If not provided, the function name is used. - as_type (Optional[Literal["generation"]]): Set to "generation" to create a specialized LLM generation span - with model metrics support, suitable for tracking language model outputs. + as_type (Optional[Literal]): Set the observation type. Supported values: + "generation", "span", "agent", "tool", "chain", "retriever", "embedding", "evaluator", "guardrail". + Observation types are highlighted in the Langfuse UI for filtering and visualization. + The types "generation" and "embedding" create a span on which additional attributes such as model metrics + can be set. Returns: Callable: A wrapped version of the original function that automatically creates and manages Langfuse spans. @@ -146,6 +162,13 @@ def sub_process(): - For async functions, the decorator returns an async function wrapper. - For sync functions, the decorator returns a synchronous wrapper. """ + valid_types = set(get_observation_types_list(ObservationTypeLiteralNoEvent)) + if as_type is not None and as_type not in valid_types: + logger.warning( + f"Invalid as_type '{as_type}'. Valid types are: {', '.join(sorted(valid_types))}. Defaulting to 'span'." + ) + as_type = "span" + function_io_capture_enabled = os.environ.get( LANGFUSE_OBSERVE_DECORATOR_IO_CAPTURE_ENABLED, "True" ).lower() not in ("false", "0") @@ -182,13 +205,13 @@ def decorator(func: F) -> F: ) """Handle decorator with or without parentheses. - + This logic enables the decorator to work both with and without parentheses: - @observe - Python passes the function directly to the decorator - @observe() - Python calls the decorator first, which must return a function decorator - + When called without arguments (@observe), the func parameter contains the function to decorate, - so we directly apply the decorator to it. When called with parentheses (@observe()), + so we directly apply the decorator to it. When called with parentheses (@observe()), func is None, so we return the decorator function itself for Python to apply in the next step. """ if func is None: @@ -201,7 +224,7 @@ def _async_observe( func: F, *, name: Optional[str], - as_type: Optional[Literal["generation"]], + as_type: Optional[ObservationTypeLiteralNoEvent], capture_input: bool, capture_output: bool, transform_to_string: Optional[Callable[[Iterable], str]] = None, @@ -231,72 +254,61 @@ async def async_wrapper(*args: Tuple[Any], **kwargs: Dict[str, Any]) -> Any: else None ) public_key = cast(str, kwargs.pop("langfuse_public_key", None)) - langfuse_client = get_client(public_key=public_key) - context_manager: Optional[ - Union[ - _AgnosticContextManager[LangfuseGeneration], - _AgnosticContextManager[LangfuseSpan], - ] - ] = ( - ( - langfuse_client.start_as_current_generation( - name=final_name, - trace_context=trace_context, - input=input, - end_on_exit=False, # when returning a generator, closing on exit would be to early - ) - if as_type == "generation" - else langfuse_client.start_as_current_span( + + # Set public key in execution context for nested decorated functions + with _set_current_public_key(public_key): + langfuse_client = get_client(public_key=public_key) + context_manager: Optional[ + Union[ + _AgnosticContextManager[LangfuseGeneration], + _AgnosticContextManager[LangfuseSpan], + _AgnosticContextManager[LangfuseAgent], + _AgnosticContextManager[LangfuseTool], + _AgnosticContextManager[LangfuseChain], + _AgnosticContextManager[LangfuseRetriever], + _AgnosticContextManager[LangfuseEvaluator], + _AgnosticContextManager[LangfuseEmbedding], + _AgnosticContextManager[LangfuseGuardrail], + ] + ] = ( + langfuse_client.start_as_current_observation( name=final_name, + as_type=as_type or "span", trace_context=trace_context, input=input, end_on_exit=False, # when returning a generator, closing on exit would be to early ) + if langfuse_client + else None ) - if langfuse_client - else None - ) - - if context_manager is None: - return await func(*args, **kwargs) - - with context_manager as langfuse_span_or_generation: - is_return_type_generator = False - - try: - result = await func(*args, **kwargs) - - if capture_output is True: - if inspect.isgenerator(result): - is_return_type_generator = True - - return self._wrap_sync_generator_result( - langfuse_span_or_generation, - result, - transform_to_string, - ) - - if inspect.isasyncgen(result): - is_return_type_generator = True - return self._wrap_async_generator_result( - langfuse_span_or_generation, - result, - transform_to_string, - ) - - langfuse_span_or_generation.update(output=result) - - return result - except Exception as e: - langfuse_span_or_generation.update( - level="ERROR", status_message=str(e) - ) - - raise e - finally: - if not is_return_type_generator: - langfuse_span_or_generation.end() + if context_manager is None: + return await func(*args, **kwargs) + + with context_manager as langfuse_span_or_generation: + is_return_type_generator = False + + try: + result = await func(*args, **kwargs) + ( + is_return_type_generator, + result, + ) = self._handle_observe_result( + langfuse_span_or_generation, + result, + capture_output=capture_output, + transform_to_string=transform_to_string, + ) + return result + except (Exception, asyncio.CancelledError) as e: + langfuse_span_or_generation.update( + level="ERROR", status_message=str(e) or type(e).__name__ + ) + + raise e + finally: + if not is_return_type_generator: + langfuse_span_or_generation.end() return cast(F, async_wrapper) @@ -305,7 +317,7 @@ def _sync_observe( func: F, *, name: Optional[str], - as_type: Optional[Literal["generation"]], + as_type: Optional[ObservationTypeLiteralNoEvent], capture_input: bool, capture_output: bool, transform_to_string: Optional[Callable[[Iterable], str]] = None, @@ -333,72 +345,61 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Any: else None ) public_key = kwargs.pop("langfuse_public_key", None) - langfuse_client = get_client(public_key=public_key) - context_manager: Optional[ - Union[ - _AgnosticContextManager[LangfuseGeneration], - _AgnosticContextManager[LangfuseSpan], - ] - ] = ( - ( - langfuse_client.start_as_current_generation( - name=final_name, - trace_context=trace_context, - input=input, - end_on_exit=False, # when returning a generator, closing on exit would be to early - ) - if as_type == "generation" - else langfuse_client.start_as_current_span( + + # Set public key in execution context for nested decorated functions + with _set_current_public_key(public_key): + langfuse_client = get_client(public_key=public_key) + context_manager: Optional[ + Union[ + _AgnosticContextManager[LangfuseGeneration], + _AgnosticContextManager[LangfuseSpan], + _AgnosticContextManager[LangfuseAgent], + _AgnosticContextManager[LangfuseTool], + _AgnosticContextManager[LangfuseChain], + _AgnosticContextManager[LangfuseRetriever], + _AgnosticContextManager[LangfuseEvaluator], + _AgnosticContextManager[LangfuseEmbedding], + _AgnosticContextManager[LangfuseGuardrail], + ] + ] = ( + langfuse_client.start_as_current_observation( name=final_name, + as_type=as_type or "span", trace_context=trace_context, input=input, end_on_exit=False, # when returning a generator, closing on exit would be to early ) + if langfuse_client + else None ) - if langfuse_client - else None - ) - - if context_manager is None: - return func(*args, **kwargs) - with context_manager as langfuse_span_or_generation: - is_return_type_generator = False - - try: - result = func(*args, **kwargs) - - if capture_output is True: - if inspect.isgenerator(result): - is_return_type_generator = True - - return self._wrap_sync_generator_result( - langfuse_span_or_generation, - result, - transform_to_string, - ) - - if inspect.isasyncgen(result): - is_return_type_generator = True - - return self._wrap_async_generator_result( - langfuse_span_or_generation, - result, - transform_to_string, - ) - - langfuse_span_or_generation.update(output=result) - - return result - except Exception as e: - langfuse_span_or_generation.update( - level="ERROR", status_message=str(e) - ) - - raise e - finally: - if not is_return_type_generator: - langfuse_span_or_generation.end() + if context_manager is None: + return func(*args, **kwargs) + + with context_manager as langfuse_span_or_generation: + is_return_type_generator = False + + try: + result = func(*args, **kwargs) + ( + is_return_type_generator, + result, + ) = self._handle_observe_result( + langfuse_span_or_generation, + result, + capture_output=capture_output, + transform_to_string=transform_to_string, + ) + return result + except (Exception, asyncio.CancelledError) as e: + langfuse_span_or_generation.update( + level="ERROR", status_message=str(e) or type(e).__name__ + ) + + raise e + finally: + if not is_return_type_generator: + langfuse_span_or_generation.end() return cast(F, sync_wrapper) @@ -426,57 +427,317 @@ def _get_input_from_func_args( def _wrap_sync_generator_result( self, - langfuse_span_or_generation: Union[LangfuseSpan, LangfuseGeneration], + langfuse_span_or_generation: Union[ + LangfuseSpan, + LangfuseGeneration, + LangfuseAgent, + LangfuseTool, + LangfuseChain, + LangfuseRetriever, + LangfuseEvaluator, + LangfuseEmbedding, + LangfuseGuardrail, + ], generator: Generator, + capture_output: bool, transform_to_string: Optional[Callable[[Iterable], str]] = None, ) -> Any: - items = [] + preserved_context = contextvars.copy_context() + + return _ContextPreservedSyncGeneratorWrapper( + generator, + preserved_context, + langfuse_span_or_generation, + capture_output, + transform_to_string, + ) + + def _wrap_async_generator_result( + self, + langfuse_span_or_generation: Union[ + LangfuseSpan, + LangfuseGeneration, + LangfuseAgent, + LangfuseTool, + LangfuseChain, + LangfuseRetriever, + LangfuseEvaluator, + LangfuseEmbedding, + LangfuseGuardrail, + ], + generator: AsyncGenerator, + capture_output: bool, + transform_to_string: Optional[Callable[[Iterable], str]] = None, + ) -> Any: + preserved_context = contextvars.copy_context() + + return _ContextPreservedAsyncGeneratorWrapper( + generator, + preserved_context, + langfuse_span_or_generation, + capture_output, + transform_to_string, + ) + + def _handle_observe_result( + self, + langfuse_span_or_generation: Union[ + LangfuseSpan, + LangfuseGeneration, + LangfuseAgent, + LangfuseTool, + LangfuseChain, + LangfuseRetriever, + LangfuseEvaluator, + LangfuseEmbedding, + LangfuseGuardrail, + ], + result: Any, + *, + capture_output: bool, + transform_to_string: Optional[Callable[[Iterable], str]] = None, + ) -> Tuple[bool, Any]: + if inspect.isgenerator(result): + return True, self._wrap_sync_generator_result( + langfuse_span_or_generation, + result, + capture_output, + transform_to_string, + ) + + if inspect.isasyncgen(result): + return True, self._wrap_async_generator_result( + langfuse_span_or_generation, + result, + capture_output, + transform_to_string, + ) + + # handle starlette.StreamingResponse + if type(result).__name__ == "StreamingResponse" and hasattr( + result, "body_iterator" + ): + result.body_iterator = self._wrap_async_generator_result( + langfuse_span_or_generation, + result.body_iterator, + capture_output, + transform_to_string, + ) + return True, result + + if capture_output is True: + langfuse_span_or_generation.update(output=result) + + return False, result + + +_decorator = LangfuseDecorator() + +observe = _decorator.observe + + +class _ContextPreservedSyncGeneratorWrapper: + """Sync generator wrapper that ensures each iteration runs in preserved context.""" + + def __init__( + self, + generator: Generator, + context: contextvars.Context, + span: Union[ + LangfuseSpan, + LangfuseGeneration, + LangfuseAgent, + LangfuseTool, + LangfuseChain, + LangfuseRetriever, + LangfuseEvaluator, + LangfuseEmbedding, + LangfuseGuardrail, + ], + capture_output: bool, + transform_fn: Optional[Callable[[Iterable], str]], + ) -> None: + self.generator = generator + self.context = context + self.items: List[Any] = [] + self.span = span + self.capture_output = capture_output + self.transform_fn = transform_fn + self._span_ended = False + + def __iter__(self) -> "_ContextPreservedSyncGeneratorWrapper": + return self + + def _finalize(self) -> None: + if self._span_ended: + return + + if self.capture_output: + output: Any = self.items + + if self.transform_fn is not None: + output = self.transform_fn(self.items) + + elif all(isinstance(item, str) for item in self.items): + output = "".join(self.items) + + self.span.update(output=output) + + self.span.end() + self._span_ended = True + + def _finalize_with_error(self, error: BaseException) -> None: + if self._span_ended: + return + + self.span.update( + level="ERROR", status_message=str(error) or type(error).__name__ + ).end() + self._span_ended = True + + def close(self) -> None: + if self._span_ended: + return try: - for item in generator: - items.append(item) + self.context.run(self.generator.close) + except (Exception, asyncio.CancelledError) as error: + self._finalize_with_error(error) + raise + else: + self._finalize() - yield item + def __del__(self) -> None: + try: + self.close() + except BaseException: + pass - finally: - output: Any = items + def __next__(self) -> Any: + try: + # Run the generator's __next__ in the preserved context + item = self.context.run(next, self.generator) + if self.capture_output: + self.items.append(item) - if transform_to_string is not None: - output = transform_to_string(items) + return item - elif all(isinstance(item, str) for item in items): - output = "".join(items) + except StopIteration: + self._finalize() + raise # Re-raise StopIteration - langfuse_span_or_generation.update(output=output) - langfuse_span_or_generation.end() + except (Exception, asyncio.CancelledError) as e: + self._finalize_with_error(e) + raise - async def _wrap_async_generator_result( + +class _ContextPreservedAsyncGeneratorWrapper: + """Async generator wrapper that ensures each iteration runs in preserved context.""" + + def __init__( self, - langfuse_span_or_generation: Union[LangfuseSpan, LangfuseGeneration], generator: AsyncGenerator, - transform_to_string: Optional[Callable[[Iterable], str]] = None, - ) -> AsyncGenerator: - items = [] + context: contextvars.Context, + span: Union[ + LangfuseSpan, + LangfuseGeneration, + LangfuseAgent, + LangfuseTool, + LangfuseChain, + LangfuseRetriever, + LangfuseEvaluator, + LangfuseEmbedding, + LangfuseGuardrail, + ], + capture_output: bool, + transform_fn: Optional[Callable[[Iterable], str]], + ) -> None: + self.generator = generator + self.context = context + self.items: List[Any] = [] + self.span = span + self.capture_output = capture_output + self.transform_fn = transform_fn + self._span_ended = False - try: - async for item in generator: - items.append(item) + def __aiter__(self) -> "_ContextPreservedAsyncGeneratorWrapper": + return self - yield item + def _finalize(self) -> None: + if self._span_ended: + return - finally: - output: Any = items + if self.capture_output: + output: Any = self.items - if transform_to_string is not None: - output = transform_to_string(items) + if self.transform_fn is not None: + output = self.transform_fn(self.items) - elif all(isinstance(item, str) for item in items): - output = "".join(items) + elif all(isinstance(item, str) for item in self.items): + output = "".join(self.items) - langfuse_span_or_generation.update(output=output) - langfuse_span_or_generation.end() + self.span.update(output=output) + self.span.end() + self._span_ended = True -_decorator = LangfuseDecorator() + def _finalize_with_error(self, error: BaseException) -> None: + if self._span_ended: + return -observe = _decorator.observe + self.span.update( + level="ERROR", status_message=str(error) or type(error).__name__ + ).end() + self._span_ended = True + + async def aclose(self) -> None: + if self._span_ended: + return + + try: + try: + await asyncio.create_task( + self.generator.aclose(), + context=self.context, + ) # type: ignore + except TypeError: + await self.context.run(asyncio.create_task, self.generator.aclose()) + except (Exception, asyncio.CancelledError) as error: + self._finalize_with_error(error) + raise + else: + self._finalize() + + async def close(self) -> None: + await self.aclose() + + def __del__(self) -> None: + self._finalize() + + async def __anext__(self) -> Any: + try: + # Run the generator's __anext__ in the preserved context + try: + # Python 3.11+ approach with explicit task context + item = await asyncio.create_task( + self.generator.__anext__(), # type: ignore + context=self.context, + ) # type: ignore + except TypeError: + # Python 3.10 fallback - create the task inside the preserved context. + item = await self.context.run( + asyncio.create_task, + self.generator.__anext__(), # type: ignore + ) + + if self.capture_output: + self.items.append(item) + + return item + + except StopAsyncIteration: + self._finalize() + raise # Re-raise StopAsyncIteration + except (Exception, asyncio.CancelledError) as e: + self._finalize_with_error(e) + raise diff --git a/langfuse/_client/propagation.py b/langfuse/_client/propagation.py new file mode 100644 index 000000000..597d8126e --- /dev/null +++ b/langfuse/_client/propagation.py @@ -0,0 +1,549 @@ +"""Attribute propagation utilities for Langfuse OpenTelemetry integration. + +This module provides the `propagate_attributes` context manager for setting trace-level +attributes (user_id, session_id, metadata) that automatically propagate to all child spans +within the context. +""" + +from typing import Any, Dict, Generator, List, Literal, Optional, TypedDict, Union, cast + +from opentelemetry import ( + baggage, +) +from opentelemetry import ( + baggage as otel_baggage_api, +) +from opentelemetry import ( + context as otel_context_api, +) +from opentelemetry import ( + trace as otel_trace_api, +) +from opentelemetry.context import _RUNTIME_CONTEXT +from opentelemetry.util._decorator import ( + _AgnosticContextManager, + _agnosticcontextmanager, +) + +from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse._client.constants import LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT +from langfuse.logger import langfuse_logger + +PropagatedKeys = Literal[ + "user_id", + "session_id", + "metadata", + "version", + "tags", + "trace_name", +] + +InternalPropagatedKeys = Literal[ + "experiment_id", + "experiment_name", + "experiment_metadata", + "experiment_dataset_id", + "experiment_item_id", + "experiment_item_metadata", + "experiment_item_root_observation_id", +] + +propagated_keys: List[Union[PropagatedKeys, InternalPropagatedKeys]] = [ + "user_id", + "session_id", + "metadata", + "version", + "tags", + "trace_name", + "experiment_id", + "experiment_name", + "experiment_metadata", + "experiment_dataset_id", + "experiment_item_id", + "experiment_item_metadata", + "experiment_item_root_observation_id", +] + + +class PropagatedExperimentAttributes(TypedDict): + experiment_id: str + experiment_name: str + experiment_metadata: Optional[Dict[str, str]] + experiment_dataset_id: Optional[str] + experiment_item_id: str + experiment_item_metadata: Optional[Dict[str, str]] + experiment_item_root_observation_id: str + + +def _detach_context_token_safely(token: Any) -> None: + """Detach a context token without emitting noisy async teardown errors. + + OpenTelemetry tokens are backed by ``contextvars`` and must be detached in the + same execution context where they were attached. Async frameworks can legitimately + end spans or unwind context managers in a different task/context, in which case + detach raises and the public OpenTelemetry helper logs an error. At that point the + observation is already completed, so the mismatch is safe to ignore. + """ + + try: + _RUNTIME_CONTEXT.detach(token) + except Exception: + pass + + +def propagate_attributes( + *, + user_id: Optional[str] = None, + session_id: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + version: Optional[str] = None, + tags: Optional[List[str]] = None, + trace_name: Optional[str] = None, + as_baggage: bool = False, +) -> _AgnosticContextManager[Any]: + """Propagate trace-level attributes to all spans created within this context. + + This context manager sets attributes on the currently active span AND automatically + propagates them to all new child spans created within the context. This is the + recommended way to set trace-level attributes like user_id, session_id, and metadata + dimensions that should be consistently applied across all observations in a trace. + + **IMPORTANT**: Call this as early as possible within your trace/workflow. Only the + currently active span and spans created after entering this context will have these + attributes. Pre-existing spans will NOT be retroactively updated. + + **Why this matters**: Langfuse aggregation queries (e.g., total cost by user_id, + filtering by session_id) only include observations that have the attribute set. + If you call `propagate_attributes` late in your workflow, earlier spans won't be + included in aggregations for that attribute. + + Args: + user_id: User identifier to associate with all spans in this context. + Must be US-ASCII string, ≤200 characters. Use this to track which user + generated each trace and enable e.g. per-user cost/performance analysis. + session_id: Session identifier to associate with all spans in this context. + Must be US-ASCII string, ≤200 characters. Use this to group related traces + within a user session (e.g., a conversation thread, multi-turn interaction). + metadata: Additional key-value metadata to propagate to all spans. + - Keys and values must be US-ASCII strings + - All values must be ≤200 characters + - Use for dimensions like internal correlating identifiers + - AVOID: large payloads, sensitive data, non-string values (will be dropped with warning) + version: Version identfier for parts of your application that are independently versioned, e.g. agents + tags: List of tags to categorize the group of observations + trace_name: Name to assign to the trace. Must be US-ASCII string, ≤200 characters. + Use this to set a consistent trace name for all spans created within this context. + as_baggage: If True, propagates attributes using OpenTelemetry baggage for + cross-process/service propagation. **Security warning**: When enabled, + attribute values are added to HTTP headers on ALL outbound requests. + Only enable if values are safe to transmit via HTTP headers and you need + cross-service tracing. Default: False. + + Returns: + Context manager that propagates attributes to all child spans. + + Example: + Basic usage with user and session tracking: + + ```python + from langfuse import Langfuse + + langfuse = Langfuse() + + # Set attributes early in the trace + with langfuse.start_as_current_observation(name="user_workflow") as span: + with langfuse.propagate_attributes( + user_id="user_123", + session_id="session_abc", + metadata={"experiment": "variant_a", "environment": "production"} + ): + # All spans created here will have user_id, session_id, and metadata + with langfuse.start_observation(name="llm_call") as llm_span: + # This span inherits: user_id, session_id, experiment, environment + ... + + with langfuse.start_generation(name="completion") as gen: + # This span also inherits all attributes + ... + ``` + + Late propagation (anti-pattern): + + ```python + with langfuse.start_as_current_observation(name="workflow") as span: + # These spans WON'T have user_id + early_span = langfuse.start_observation(name="early_work") + early_span.end() + + # Set attributes in the middle + with langfuse.propagate_attributes(user_id="user_123"): + # Only spans created AFTER this point will have user_id + late_span = langfuse.start_observation(name="late_work") + late_span.end() + + # Result: Aggregations by user_id will miss "early_work" span + ``` + + Cross-service propagation with baggage (advanced): + + ```python + # Service A - originating service + with langfuse.start_as_current_observation(name="api_request"): + with langfuse.propagate_attributes( + user_id="user_123", + session_id="session_abc", + as_baggage=True # Propagate via HTTP headers + ): + # Make HTTP request to Service B + response = requests.get("https://service-b.example.com/api") + # user_id and session_id are now in HTTP headers + + # Service B - downstream service + # OpenTelemetry will automatically extract baggage from HTTP headers + # and propagate to spans in Service B + ``` + + Note: + - **Validation**: All attribute values (user_id, session_id, metadata values) + must be strings ≤200 characters. Invalid values will be dropped with a + warning logged. Ensure values meet constraints before calling. + - **OpenTelemetry**: This uses OpenTelemetry context propagation under the hood, + making it compatible with other OTel-instrumented libraries. + + Raises: + No exceptions are raised. Invalid values are logged as warnings and dropped. + """ + return _propagate_attributes( + user_id=user_id, + session_id=session_id, + metadata=metadata, + version=version, + tags=tags, + trace_name=trace_name, + as_baggage=as_baggage, + ) + + +@_agnosticcontextmanager +def _propagate_attributes( + *, + user_id: Optional[str] = None, + session_id: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + version: Optional[str] = None, + tags: Optional[List[str]] = None, + trace_name: Optional[str] = None, + as_baggage: bool = False, + experiment: Optional[PropagatedExperimentAttributes] = None, +) -> Generator[Any, Any, Any]: + context = otel_context_api.get_current() + current_span = otel_trace_api.get_current_span() + + propagated_string_attributes: Dict[str, Optional[Union[str, List[str]]]] = { + "user_id": user_id, + "session_id": session_id, + "version": version, + "tags": tags, + "trace_name": trace_name, + } + + propagated_metadata_attributes: Dict[str, Optional[Dict[str, str]]] = { + "metadata": metadata, + } + + if experiment: + for key, value in experiment.items(): + if key in ("experiment_metadata", "experiment_item_metadata"): + propagated_metadata_attributes[key] = cast( + Optional[Dict[str, str]], value + ) + else: + propagated_string_attributes[key] = cast( + Optional[Union[str, List[str]]], value + ) + + # Filter out None values + propagated_string_attributes = { + k: v for k, v in propagated_string_attributes.items() if v is not None + } + + for key, value in propagated_string_attributes.items(): + validated_value = _validate_propagated_value(value=value, key=key) + + if validated_value is not None: + context = _set_propagated_attribute( + key=key, + value=validated_value, + context=context, + span=current_span, + as_baggage=as_baggage, + ) + + for metadata_key, metadata_value in propagated_metadata_attributes.items(): + if metadata_value is None: + continue + + validated_metadata: Dict[str, str] = {} + + for key, value in metadata_value.items(): + if _validate_string_value(value=value, key=f"{metadata_key}.{key}"): + validated_metadata[key] = value + + if validated_metadata: + context = _set_propagated_attribute( + key=metadata_key, + value=validated_metadata, + context=context, + span=current_span, + as_baggage=as_baggage, + ) + + # Activate context, execute, and detach context + token = otel_context_api.attach(context=context) + + try: + yield + + finally: + _detach_context_token_safely(token) + + +def _get_propagated_attributes_from_context( + context: otel_context_api.Context, +) -> Dict[str, Union[str, List[str]]]: + propagated_attributes: Dict[str, Union[str, List[str]]] = {} + + # Handle baggage + baggage_entries = baggage.get_all(context=context) + for baggage_key, baggage_value in baggage_entries.items(): + if baggage_key == LANGFUSE_TRACE_ID_BAGGAGE_KEY: + continue + + if baggage_key.startswith(LANGFUSE_BAGGAGE_PREFIX): + span_key = _get_span_key_from_baggage_key(baggage_key) + + if span_key: + propagated_attributes[span_key] = ( + baggage_value + if isinstance(baggage_value, (str, list)) + else str(baggage_value) + ) + + # Handle OTEL context + for key in propagated_keys: + context_key = _get_propagated_context_key(key) + value = otel_context_api.get_value(key=context_key, context=context) + + if value is None: + continue + + if isinstance(value, dict): + # Handle metadata + span_key = _get_propagated_span_key(key) + + for k, v in value.items(): + propagated_attributes[f"{span_key}.{k}"] = v + + else: + span_key = _get_propagated_span_key(key) + + propagated_attributes[span_key] = ( + value if isinstance(value, (str, list)) else str(value) + ) + + if ( + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID + in propagated_attributes + ): + propagated_attributes[LangfuseOtelSpanAttributes.ENVIRONMENT] = ( + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT + ) + + return propagated_attributes + + +def _set_propagated_attribute( + *, + key: str, + value: Union[str, List[str], Dict[str, str]], + context: otel_context_api.Context, + span: otel_trace_api.Span, + as_baggage: bool, +) -> otel_context_api.Context: + # Get key names + context_key = _get_propagated_context_key(key) + span_key = _get_propagated_span_key(key) + baggage_key = _get_propagated_baggage_key(key) + + # Merge metadata with previously set metadata keys + if isinstance(value, dict): + existing_metadata_in_context = cast( + dict, otel_context_api.get_value(context_key) or {} + ) + value = existing_metadata_in_context | value + + # Merge tags with previously set tags + if isinstance(value, list): + existing_tags_in_context = cast( + list, otel_context_api.get_value(context_key) or [] + ) + merged_tags = list(existing_tags_in_context) + merged_tags.extend(tag for tag in value if tag not in existing_tags_in_context) + + value = merged_tags + + # Set in context + context = otel_context_api.set_value( + key=context_key, + value=value, + context=context, + ) + + # Set on current span + if span is not None and span.is_recording(): + if isinstance(value, dict): + # Handle metadata + for k, v in value.items(): + span.set_attribute( + key=f"{span_key}.{k}", + value=v, + ) + + else: + span.set_attribute(key=span_key, value=value) + + # Set on baggage + if as_baggage: + if isinstance(value, dict): + # Handle metadata + for k, v in value.items(): + context = otel_baggage_api.set_baggage( + name=f"{baggage_key}_{k}", value=v, context=context + ) + else: + context = otel_baggage_api.set_baggage( + name=baggage_key, value=value, context=context + ) + + return context + + +def _validate_propagated_value( + *, value: Any, key: str +) -> Optional[Union[str, List[str]]]: + if isinstance(value, list): + validated_values = [ + v for v in value if _validate_string_value(key=key, value=v) + ] + + return validated_values if len(validated_values) > 0 else None + + if not isinstance(value, str): + langfuse_logger.warning( # type: ignore + f"Propagated attribute '{key}' value is not a string. Dropping value." + ) + return None + + if len(value) > 200: + langfuse_logger.warning( + f"Propagated attribute '{key}' value is over 200 characters ({len(value)} chars). Dropping value." + ) + return None + + return value + + +def _validate_string_value(*, value: str, key: str) -> bool: + if not isinstance(value, str): + langfuse_logger.warning( # type: ignore + f"Propagated attribute '{key}' value is not a string. Dropping value." + ) + return False + + if len(value) > 200: + langfuse_logger.warning( + f"Propagated attribute '{key}' value is over 200 characters ({len(value)} chars). Dropping value." + ) + return False + + return True + + +def _get_propagated_context_key(key: str) -> str: + return f"langfuse.propagated.{key}" + + +LANGFUSE_BAGGAGE_PREFIX = "langfuse_" +LANGFUSE_TRACE_ID_BAGGAGE_KEY = "langfuse_trace_id" + + +def _get_propagated_baggage_key(key: str) -> str: + return f"{LANGFUSE_BAGGAGE_PREFIX}{key}" + + +def _get_langfuse_trace_id_from_baggage( + context: otel_context_api.Context, +) -> Optional[str]: + value = otel_baggage_api.get_baggage( + name=LANGFUSE_TRACE_ID_BAGGAGE_KEY, + context=context, + ) + + if value is None: + return None + + return str(value).lower() + + +def _set_langfuse_trace_id_in_baggage( + *, + trace_id: str, + context: otel_context_api.Context, +) -> otel_context_api.Context: + normalized_trace_id = trace_id.lower() + + if _get_langfuse_trace_id_from_baggage(context) == normalized_trace_id: + return context + + return otel_baggage_api.set_baggage( + name=LANGFUSE_TRACE_ID_BAGGAGE_KEY, + value=normalized_trace_id, + context=context, + ) + + +def _get_span_key_from_baggage_key(key: str) -> Optional[str]: + if not key.startswith(LANGFUSE_BAGGAGE_PREFIX): + return None + + # Remove prefix to get the actual key name + suffix = key[len(LANGFUSE_BAGGAGE_PREFIX) :] + + for metadata_key in ("metadata", "experiment_metadata", "experiment_item_metadata"): + baggage_metadata_prefix = f"{metadata_key}_" + + if suffix.startswith(baggage_metadata_prefix): + return ( + f"{_get_propagated_span_key(metadata_key)}." + f"{suffix[len(baggage_metadata_prefix) :]}" + ) + + return _get_propagated_span_key(suffix) + + +def _get_propagated_span_key(key: str) -> str: + return { + "session_id": LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "user_id": LangfuseOtelSpanAttributes.TRACE_USER_ID, + "version": LangfuseOtelSpanAttributes.VERSION, + "tags": LangfuseOtelSpanAttributes.TRACE_TAGS, + "trace_name": LangfuseOtelSpanAttributes.TRACE_NAME, + "metadata": LangfuseOtelSpanAttributes.TRACE_METADATA, + "experiment_id": LangfuseOtelSpanAttributes.EXPERIMENT_ID, + "experiment_name": LangfuseOtelSpanAttributes.EXPERIMENT_NAME, + "experiment_metadata": LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, + "experiment_dataset_id": LangfuseOtelSpanAttributes.EXPERIMENT_DATASET_ID, + "experiment_item_id": LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ID, + "experiment_item_metadata": LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA, + "experiment_item_root_observation_id": LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID, + }.get(key) or f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.{key}" diff --git a/langfuse/_client/resource_manager.py b/langfuse/_client/resource_manager.py index e0e3cbadc..2d42f6ce1 100644 --- a/langfuse/_client/resource_manager.py +++ b/langfuse/_client/resource_manager.py @@ -18,12 +18,13 @@ import os import threading from queue import Full, Queue -from typing import Any, Dict, List, Optional, cast +from typing import Any, Callable, Dict, List, Optional, cast import httpx from opentelemetry import trace as otel_trace_api from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.sdk.trace.sampling import Decision, TraceIdRatioBased from opentelemetry.trace import Tracer @@ -42,11 +43,11 @@ from langfuse._utils.environment import get_common_release_envs from langfuse._utils.prompt_cache import PromptCache from langfuse._utils.request import LangfuseClient -from langfuse.api.client import AsyncFernLangfuse, FernLangfuse +from langfuse.api import AsyncLangfuseAPI, LangfuseAPI from langfuse.logger import langfuse_logger from langfuse.types import MaskFunction -from ..version import __version__ as langfuse_version +from .._version import __version__ as langfuse_version class LangfuseResourceManager: @@ -83,7 +84,7 @@ def __new__( *, public_key: str, secret_key: str, - host: str, + base_url: str, environment: Optional[str] = None, release: Optional[str] = None, timeout: Optional[int] = None, @@ -95,8 +96,10 @@ def __new__( mask: Optional[MaskFunction] = None, tracing_enabled: Optional[bool] = None, blocked_instrumentation_scopes: Optional[List[str]] = None, + should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, additional_headers: Optional[Dict[str, str]] = None, tracer_provider: Optional[TracerProvider] = None, + span_exporter: Optional[SpanExporter] = None, ) -> "LangfuseResourceManager": if public_key in cls._instances: return cls._instances[public_key] @@ -115,7 +118,7 @@ def __new__( instance._initialize_instance( public_key=public_key, secret_key=secret_key, - host=host, + base_url=base_url, timeout=timeout, environment=environment, release=release, @@ -129,8 +132,10 @@ def __new__( if tracing_enabled is not None else True, blocked_instrumentation_scopes=blocked_instrumentation_scopes, + should_export_span=should_export_span, additional_headers=additional_headers, tracer_provider=tracer_provider, + span_exporter=span_exporter, ) cls._instances[public_key] = instance @@ -142,7 +147,7 @@ def _initialize_instance( *, public_key: str, secret_key: str, - host: str, + base_url: str, environment: Optional[str] = None, release: Optional[str] = None, timeout: Optional[int] = None, @@ -154,30 +159,49 @@ def _initialize_instance( mask: Optional[MaskFunction] = None, tracing_enabled: bool = True, blocked_instrumentation_scopes: Optional[List[str]] = None, + should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, additional_headers: Optional[Dict[str, str]] = None, tracer_provider: Optional[TracerProvider] = None, + span_exporter: Optional[SpanExporter] = None, ) -> None: self.public_key = public_key self.secret_key = secret_key self.tracing_enabled = tracing_enabled - self.host = host + self.base_url = base_url self.mask = mask + self.environment = environment + + # Store additional client settings for get_client() to use + self.timeout = timeout + self.flush_at = flush_at + self.flush_interval = flush_interval + self.release = release + self.media_upload_thread_count = media_upload_thread_count + self.sample_rate = sample_rate + self.blocked_instrumentation_scopes = blocked_instrumentation_scopes + self.should_export_span = should_export_span + self.additional_headers = additional_headers + self.span_exporter = span_exporter + self.tracer_provider: Optional[TracerProvider] = None # OTEL Tracer if tracing_enabled: tracer_provider = tracer_provider or _init_tracer_provider( environment=environment, release=release, sample_rate=sample_rate ) + self.tracer_provider = tracer_provider langfuse_processor = LangfuseSpanProcessor( public_key=self.public_key, secret_key=secret_key, - host=host, + base_url=base_url, timeout=timeout, flush_at=flush_at, flush_interval=flush_interval, blocked_instrumentation_scopes=blocked_instrumentation_scopes, + should_export_span=should_export_span, additional_headers=additional_headers, + span_exporter=span_exporter, ) tracer_provider.add_span_processor(langfuse_processor) @@ -200,8 +224,8 @@ def _initialize_instance( client_headers = additional_headers if additional_headers else {} self.httpx_client = httpx.Client(timeout=timeout, headers=client_headers) - self.api = FernLangfuse( - base_url=host, + self.api = LangfuseAPI( + base_url=base_url, username=self.public_key, password=secret_key, x_langfuse_sdk_name="python", @@ -210,8 +234,8 @@ def _initialize_instance( httpx_client=self.httpx_client, timeout=timeout, ) - self.async_api = AsyncFernLangfuse( - base_url=host, + self.async_api = AsyncLangfuseAPI( + base_url=base_url, username=self.public_key, password=secret_key, x_langfuse_sdk_name="python", @@ -222,7 +246,7 @@ def _initialize_instance( score_ingestion_client = LangfuseClient( public_key=self.public_key, secret_key=secret_key, - base_url=host, + base_url=base_url, version=langfuse_version, timeout=timeout or 20, session=self.httpx_client, @@ -236,6 +260,7 @@ def _initialize_instance( self._media_upload_queue: Queue[Any] = Queue(100_000) self._media_manager = MediaManager( api_client=self.api, + httpx_client=self.httpx_client, media_upload_queue=self._media_upload_queue, max_retries=3, ) @@ -279,7 +304,7 @@ def _initialize_instance( langfuse_logger.info( f"Startup: Langfuse tracer successfully initialized | " f"public_key={self.public_key} | " - f"host={host} | " + f"base_url={base_url} | " f"environment={environment or 'default'} | " f"sample_rate={sample_rate if sample_rate is not None else 1.0} | " f"media_threads={media_upload_thread_count or 1}" @@ -338,6 +363,29 @@ def add_score_task(self, event: dict, *, force_sample: bool = False) -> None: return + def add_trace_task( + self, + event: dict, + ) -> None: + try: + langfuse_logger.debug( + f"Trace: Enqueuing event type={event['type']} for trace_id={event['body'].id}" + ) + self._score_ingestion_queue.put(event, block=False) + + except Full: + langfuse_logger.warning( + "System overload: Trace ingestion queue has reached capacity (100,000 items). Trace update will be dropped. Consider increasing flush frequency or decreasing event volume." + ) + + return + except Exception as e: + langfuse_logger.error( + f"Unexpected error: Failed to process trace event. The trace update will be dropped. Error details: {e}" + ) + + return + @property def tracer(self) -> Optional[Tracer]: return self._otel_tracer @@ -357,6 +405,8 @@ def _stop_and_join_consumer_threads(self) -> None: for media_upload_consumer in self._media_upload_consumers: media_upload_consumer.pause() + self._media_manager.signal_shutdown(count=len(self._media_upload_consumers)) + for media_upload_consumer in self._media_upload_consumers: try: media_upload_consumer.join() @@ -386,12 +436,11 @@ def _stop_and_join_consumer_threads(self) -> None: ) def flush(self) -> None: - tracer_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider()) - if isinstance(tracer_provider, otel_trace_api.ProxyTracerProvider): - return - - tracer_provider.force_flush() - langfuse_logger.debug("Successfully flushed OTEL tracer provider") + if self.tracer_provider is not None and not isinstance( + self.tracer_provider, otel_trace_api.ProxyTracerProvider + ): + self.tracer_provider.force_flush() + langfuse_logger.debug("Successfully flushed OTEL tracer provider") self._score_ingestion_queue.join() langfuse_logger.debug("Successfully flushed score ingestion queue") @@ -403,12 +452,7 @@ def shutdown(self) -> None: # Unregister the atexit handler first atexit.unregister(self.shutdown) - tracer_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider()) - if isinstance(tracer_provider, otel_trace_api.ProxyTracerProvider): - return - - tracer_provider.force_flush() - + self.flush() self._stop_and_join_consumer_threads() diff --git a/langfuse/_client/span.py b/langfuse/_client/span.py index 34aa4f0d1..bd0c638a7 100644 --- a/langfuse/_client/span.py +++ b/langfuse/_client/span.py @@ -5,7 +5,7 @@ creating, updating, and scoring various types of spans used in AI application tracing. Classes: -- LangfuseSpanWrapper: Abstract base class for all Langfuse spans +- LangfuseObservationWrapper: Abstract base class for all Langfuse spans - LangfuseSpan: Implementation for general-purpose spans - LangfuseGeneration: Specialized span implementation for LLM generations @@ -19,15 +19,16 @@ TYPE_CHECKING, Any, Dict, - List, Literal, Optional, + Type, Union, cast, overload, ) from opentelemetry import trace as otel_trace_api +from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util._decorator import _AgnosticContextManager from langfuse.model import PromptClient @@ -35,17 +36,32 @@ if TYPE_CHECKING: from langfuse._client.client import Langfuse +from typing_extensions import deprecated + from langfuse._client.attributes import ( LangfuseOtelSpanAttributes, create_generation_attributes, create_span_attributes, create_trace_attributes, ) +from langfuse._client.constants import ( + ObservationTypeGenerationLike, + ObservationTypeLiteral, + ObservationTypeLiteralNoEvent, + ObservationTypeSpanLike, + get_observation_types_list, +) +from langfuse.api import MapValue, ScoreDataType from langfuse.logger import langfuse_logger -from langfuse.types import MapValue, ScoreDataType, SpanLevel +from langfuse.types import SpanLevel +# Factory mapping for observation classes +# Note: "event" is handled separately due to special instantiation logic +# Populated after class definitions +_OBSERVATION_CLASS_MAP: Dict[str, Type["LangfuseObservationWrapper"]] = {} -class LangfuseSpanWrapper: + +class LangfuseObservationWrapper: """Abstract base class for all Langfuse span types. This class provides common functionality for all Langfuse span types, including @@ -64,11 +80,12 @@ def __init__( *, otel_span: otel_trace_api.Span, langfuse_client: "Langfuse", - as_type: Literal["span", "generation", "event"], + as_type: ObservationTypeLiteral, input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, environment: Optional[str] = None, + release: Optional[str] = None, version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, @@ -89,6 +106,7 @@ def __init__( output: Output data from the span (any JSON-serializable object) metadata: Additional metadata to associate with the span environment: The tracing environment + release: Release identifier for the application version: Version identifier for the code or component level: Importance level of the span (info, warning, error) status_message: Optional status message for the span @@ -104,16 +122,23 @@ def __init__( LangfuseOtelSpanAttributes.OBSERVATION_TYPE, as_type ) self._langfuse_client = langfuse_client + self._observation_type = as_type self.trace_id = self._langfuse_client._get_otel_trace_id(otel_span) self.id = self._langfuse_client._get_otel_span_id(otel_span) - self._environment = environment + self._environment = environment or self._langfuse_client._environment if self._environment is not None: self._otel_span.set_attribute( LangfuseOtelSpanAttributes.ENVIRONMENT, self._environment ) + self._release = release or self._langfuse_client._release + if self._release is not None: + self._otel_span.set_attribute( + LangfuseOtelSpanAttributes.RELEASE, self._release + ) + # Handle media only if span is sampled if self._otel_span.is_recording(): media_processed_input = self._process_media_and_apply_mask( @@ -128,7 +153,7 @@ def __init__( attributes = {} - if as_type == "generation": + if as_type in get_observation_types_list(ObservationTypeGenerationLike): attributes = create_generation_attributes( input=media_processed_input, output=media_processed_output, @@ -142,9 +167,14 @@ def __init__( usage_details=usage_details, cost_details=cost_details, prompt=prompt, + observation_type=cast( + ObservationTypeGenerationLike, + as_type, + ), ) else: + # For span-like types and events attributes = create_span_attributes( input=media_processed_input, output=media_processed_output, @@ -152,15 +182,28 @@ def __init__( version=version, level=level, status_message=status_message, + observation_type=cast( + Optional[Union[ObservationTypeSpanLike, Literal["event"]]], + as_type + if as_type + in get_observation_types_list(ObservationTypeSpanLike) + or as_type == "event" + else None, + ), ) + # We don't want to overwrite the observation type, and already set it attributes.pop(LangfuseOtelSpanAttributes.OBSERVATION_TYPE, None) self._otel_span.set_attributes( {k: v for k, v in attributes.items() if v is not None} ) + # Set OTEL span status if level is ERROR + self._set_otel_span_status_if_error( + level=level, status_message=status_message + ) - def end(self, *, end_time: Optional[int] = None) -> "LangfuseSpanWrapper": + def end(self, *, end_time: Optional[int] = None) -> "LangfuseObservationWrapper": """End the span, marking it as completed. This method ends the wrapped OpenTelemetry span, marking the end of the @@ -174,35 +217,33 @@ def end(self, *, end_time: Optional[int] = None) -> "LangfuseSpanWrapper": return self - def update_trace( + @deprecated( + "Trace-level input/output is deprecated. " + "For trace attributes (user_id, session_id, tags, etc.), use propagate_attributes() instead. " + "This method will be removed in a future major version." + ) + def set_trace_io( self, *, - name: Optional[str] = None, - user_id: Optional[str] = None, - session_id: Optional[str] = None, - version: Optional[str] = None, input: Optional[Any] = None, output: Optional[Any] = None, - metadata: Optional[Any] = None, - tags: Optional[List[str]] = None, - public: Optional[bool] = None, - ) -> "LangfuseSpanWrapper": - """Update the trace that this span belongs to. + ) -> "LangfuseObservationWrapper": + """Set trace-level input and output for the trace this span belongs to. - This method updates trace-level attributes of the trace that this span - belongs to. This is useful for adding or modifying trace-wide information - like user ID, session ID, or tags. + .. deprecated:: + This is a legacy method for backward compatibility with Langfuse platform + features that still rely on trace-level input/output (e.g., legacy LLM-as-a-judge + evaluators). It will be removed in a future major version. + + For setting other trace attributes (user_id, session_id, metadata, tags, version), + use :meth:`Langfuse.propagate_attributes` instead. Args: - name: Updated name for the trace - user_id: ID of the user who initiated the trace - session_id: Session identifier for grouping related traces - version: Version identifier for the application or service - input: Input data for the overall trace - output: Output data from the overall trace - metadata: Additional metadata to associate with the trace - tags: List of tags to categorize the trace - public: Whether the trace should be publicly accessible + input: Input data to associate with the trace. + output: Output data to associate with the trace. + + Returns: + The span instance for method chaining. """ if not self._otel_span.is_recording(): return self @@ -213,26 +254,36 @@ def update_trace( media_processed_output = self._process_media_and_apply_mask( data=output, field="output", span=self._otel_span ) - media_processed_metadata = self._process_media_and_apply_mask( - data=metadata, field="metadata", span=self._otel_span - ) attributes = create_trace_attributes( - name=name, - user_id=user_id, - session_id=session_id, - version=version, input=media_processed_input, output=media_processed_output, - metadata=media_processed_metadata, - tags=tags, - public=public, ) self._otel_span.set_attributes(attributes) return self + def set_trace_as_public(self) -> "LangfuseObservationWrapper": + """Make this trace publicly accessible via its URL. + + When a trace is published, anyone with the trace link can view the full trace + without needing to be logged in to Langfuse. This action cannot be undone + programmatically - once any span in a trace is published, the entire trace + becomes public. + + Returns: + The span instance for method chaining. + """ + if not self._otel_span.is_recording(): + return self + + attributes = create_trace_attributes(public=True) + + self._otel_span.set_attributes(attributes) + + return self + @overload def score( self, @@ -240,9 +291,13 @@ def score( name: str, value: float, score_id: Optional[str] = None, - data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None, + data_type: Optional[ + Literal[ScoreDataType.NUMERIC, ScoreDataType.BOOLEAN] + ] = None, comment: Optional[str] = None, config_id: Optional[str] = None, + timestamp: Optional[datetime] = None, + metadata: Optional[Any] = None, ) -> None: ... @overload @@ -252,9 +307,13 @@ def score( name: str, value: str, score_id: Optional[str] = None, - data_type: Optional[Literal["CATEGORICAL"]] = "CATEGORICAL", + data_type: Optional[ + Literal[ScoreDataType.CATEGORICAL, ScoreDataType.TEXT] + ] = ScoreDataType.CATEGORICAL, comment: Optional[str] = None, config_id: Optional[str] = None, + timestamp: Optional[datetime] = None, + metadata: Optional[Any] = None, ) -> None: ... def score( @@ -266,6 +325,8 @@ def score( data_type: Optional[ScoreDataType] = None, comment: Optional[str] = None, config_id: Optional[str] = None, + timestamp: Optional[datetime] = None, + metadata: Optional[Any] = None, ) -> None: """Create a score for this specific span. @@ -274,15 +335,17 @@ def score( Args: name: Name of the score (e.g., "relevance", "accuracy") - value: Score value (numeric for NUMERIC/BOOLEAN, string for CATEGORICAL) + value: Score value (numeric for NUMERIC/BOOLEAN, string for CATEGORICAL/TEXT) score_id: Optional custom ID for the score (auto-generated if not provided) - data_type: Type of score (NUMERIC, BOOLEAN, or CATEGORICAL) + data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, or TEXT) comment: Optional comment or explanation for the score config_id: Optional ID of a score config defined in Langfuse + timestamp: Optional timestamp for the score (defaults to current UTC time) + metadata: Optional metadata to be attached to the score Example: ```python - with langfuse.start_as_current_span(name="process-query") as span: + with langfuse.start_as_current_observation(name="process-query") as span: # Do work result = process_data() @@ -301,9 +364,11 @@ def score( trace_id=self.trace_id, observation_id=self.id, score_id=score_id, - data_type=cast(Literal["CATEGORICAL"], data_type), + data_type=cast(Literal["CATEGORICAL", "TEXT"], data_type), comment=comment, config_id=config_id, + timestamp=timestamp, + metadata=metadata, ) @overload @@ -313,9 +378,13 @@ def score_trace( name: str, value: float, score_id: Optional[str] = None, - data_type: Optional[Literal["NUMERIC", "BOOLEAN"]] = None, + data_type: Optional[ + Literal[ScoreDataType.NUMERIC, ScoreDataType.BOOLEAN] + ] = None, comment: Optional[str] = None, config_id: Optional[str] = None, + timestamp: Optional[datetime] = None, + metadata: Optional[Any] = None, ) -> None: ... @overload @@ -325,9 +394,13 @@ def score_trace( name: str, value: str, score_id: Optional[str] = None, - data_type: Optional[Literal["CATEGORICAL"]] = "CATEGORICAL", + data_type: Optional[ + Literal[ScoreDataType.CATEGORICAL, ScoreDataType.TEXT] + ] = ScoreDataType.CATEGORICAL, comment: Optional[str] = None, config_id: Optional[str] = None, + timestamp: Optional[datetime] = None, + metadata: Optional[Any] = None, ) -> None: ... def score_trace( @@ -339,6 +412,8 @@ def score_trace( data_type: Optional[ScoreDataType] = None, comment: Optional[str] = None, config_id: Optional[str] = None, + timestamp: Optional[datetime] = None, + metadata: Optional[Any] = None, ) -> None: """Create a score for the entire trace that this span belongs to. @@ -348,15 +423,17 @@ def score_trace( Args: name: Name of the score (e.g., "user_satisfaction", "overall_quality") - value: Score value (numeric for NUMERIC/BOOLEAN, string for CATEGORICAL) + value: Score value (numeric for NUMERIC/BOOLEAN, string for CATEGORICAL/TEXT) score_id: Optional custom ID for the score (auto-generated if not provided) - data_type: Type of score (NUMERIC, BOOLEAN, or CATEGORICAL) + data_type: Type of score (NUMERIC, BOOLEAN, CATEGORICAL, or TEXT) comment: Optional comment or explanation for the score config_id: Optional ID of a score config defined in Langfuse + timestamp: Optional timestamp for the score (defaults to current UTC time) + metadata: Optional metadata to be attached to the score Example: ```python - with langfuse.start_as_current_span(name="handle-request") as span: + with langfuse.start_as_current_observation(name="handle-request") as span: # Process the complete request result = process_request() @@ -374,16 +451,18 @@ def score_trace( value=cast(str, value), trace_id=self.trace_id, score_id=score_id, - data_type=cast(Literal["CATEGORICAL"], data_type), + data_type=cast(Literal["CATEGORICAL", "TEXT"], data_type), comment=comment, config_id=config_id, + timestamp=timestamp, + metadata=metadata, ) def _set_processed_span_attributes( self, *, span: otel_trace_api.Span, - as_type: Optional[Literal["span", "generation", "event"]] = None, + as_type: Optional[ObservationTypeLiteral] = None, input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, @@ -511,54 +590,27 @@ def _process_media_in_attribute( return data + def _set_otel_span_status_if_error( + self, *, level: Optional[SpanLevel] = None, status_message: Optional[str] = None + ) -> None: + """Set OpenTelemetry span status to ERROR if level is ERROR. -class LangfuseSpan(LangfuseSpanWrapper): - """Standard span implementation for general operations in Langfuse. - - This class represents a general-purpose span that can be used to trace - any operation in your application. It extends the base LangfuseSpanWrapper - with specific methods for creating child spans, generations, and updating - span-specific attributes. - """ - - def __init__( - self, - *, - otel_span: otel_trace_api.Span, - langfuse_client: "Langfuse", - input: Optional[Any] = None, - output: Optional[Any] = None, - metadata: Optional[Any] = None, - environment: Optional[str] = None, - version: Optional[str] = None, - level: Optional[SpanLevel] = None, - status_message: Optional[str] = None, - ): - """Initialize a new LangfuseSpan. + This method sets the underlying OpenTelemetry span status to ERROR when the + Langfuse observation level is set to ERROR, ensuring consistency between + Langfuse and OpenTelemetry error states. Args: - otel_span: The OpenTelemetry span to wrap - langfuse_client: Reference to the parent Langfuse client - input: Input data for the span (any JSON-serializable object) - output: Output data from the span (any JSON-serializable object) - metadata: Additional metadata to associate with the span - environment: The tracing environment - version: Version identifier for the code or component - level: Importance level of the span (info, warning, error) - status_message: Optional status message for the span + level: The span level to check + status_message: Optional status message to include as description """ - super().__init__( - otel_span=otel_span, - as_type="span", - langfuse_client=langfuse_client, - input=input, - output=output, - metadata=metadata, - environment=environment, - version=version, - level=level, - status_message=status_message, - ) + if level == "ERROR" and self._otel_span.is_recording(): + try: + self._otel_span.set_status( + Status(StatusCode.ERROR, description=status_message) + ) + except Exception: + # Silently ignore any errors when setting OTEL status to avoid existing flow disruptions + pass def update( self, @@ -570,33 +622,34 @@ def update( version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, **kwargs: Any, - ) -> "LangfuseSpan": - """Update this span with new information. + ) -> "LangfuseObservationWrapper": + """Update this observation with new information. - This method updates the span with new information that becomes available + This method updates the observation with new information that becomes available during execution, such as outputs, metadata, or status changes. Args: - name: Span name + name: Observation name input: Updated input data for the operation output: Output data from the operation - metadata: Additional metadata to associate with the span + metadata: Additional metadata to associate with the observation version: Version identifier for the code or component - level: Importance level of the span (info, warning, error) - status_message: Optional status message for the span + level: Importance level of the observation (info, warning, error) + status_message: Optional status message for the observation + completion_start_time: When the generation started (for generation types) + model: Model identifier used (for generation types) + model_parameters: Parameters passed to the model (for generation types) + usage_details: Token or other usage statistics (for generation types) + cost_details: Cost breakdown for the operation (for generation types) + prompt: Reference to the prompt used (for generation types) **kwargs: Additional keyword arguments (ignored) - - Example: - ```python - span = langfuse.start_span(name="process-data") - try: - # Do work - result = process_data() - span.update(output=result, metadata={"processing_time": 350}) - finally: - span.end() - ``` """ if not self._otel_span.is_recording(): return self @@ -614,147 +667,162 @@ def update( if name: self._otel_span.update_name(name) - attributes = create_span_attributes( - input=processed_input, - output=processed_output, - metadata=processed_metadata, - version=version, - level=level, - status_message=status_message, - ) + if self._observation_type in get_observation_types_list( + ObservationTypeGenerationLike + ): + attributes = create_generation_attributes( + input=processed_input, + output=processed_output, + metadata=processed_metadata, + version=version, + level=level, + status_message=status_message, + observation_type=cast( + ObservationTypeGenerationLike, + self._observation_type, + ), + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, + ) + else: + # For span-like types and events + attributes = create_span_attributes( + input=processed_input, + output=processed_output, + metadata=processed_metadata, + version=version, + level=level, + status_message=status_message, + observation_type=cast( + Optional[Union[ObservationTypeSpanLike, Literal["event"]]], + self._observation_type + if self._observation_type + in get_observation_types_list(ObservationTypeSpanLike) + or self._observation_type == "event" + else None, + ), + ) self._otel_span.set_attributes(attributes=attributes) + # Set OTEL span status if level is ERROR + self._set_otel_span_status_if_error(level=level, status_message=status_message) return self - def start_span( + @overload + def start_observation( self, + *, name: str, + as_type: Literal["span"], input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, - ) -> "LangfuseSpan": - """Create a new child span. - - This method creates a new child span with this span as the parent. - Unlike start_as_current_span(), this method does not set the new span - as the current span in the context. - - Args: - name: Name of the span (e.g., function or operation name) - input: Input data for the operation - output: Output data from the operation - metadata: Additional metadata to associate with the span - version: Version identifier for the code or component - level: Importance level of the span (info, warning, error) - status_message: Optional status message for the span - - Returns: - A new LangfuseSpan that must be ended with .end() when complete - - Example: - ```python - parent_span = langfuse.start_span(name="process-request") - try: - # Create a child span - child_span = parent_span.start_span(name="validate-input") - try: - # Do validation work - validation_result = validate(request_data) - child_span.update(output=validation_result) - finally: - child_span.end() - - # Continue with parent span - result = process_validated_data(validation_result) - parent_span.update(output=result) - finally: - parent_span.end() - ``` - """ - with otel_trace_api.use_span(self._otel_span): - new_otel_span = self._langfuse_client._otel_tracer.start_span(name=name) + ) -> "LangfuseSpan": ... - return LangfuseSpan( - otel_span=new_otel_span, - langfuse_client=self._langfuse_client, - environment=self._environment, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ) + @overload + def start_observation( + self, + *, + name: str, + as_type: Literal["generation"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + ) -> "LangfuseGeneration": ... - def start_as_current_span( + @overload + def start_observation( self, *, name: str, + as_type: Literal["agent"], input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, - ) -> _AgnosticContextManager["LangfuseSpan"]: - """Create a new child span and set it as the current span in a context manager. + ) -> "LangfuseAgent": ... - This method creates a new child span and sets it as the current span within - a context manager. It should be used with a 'with' statement to automatically - manage the span's lifecycle. + @overload + def start_observation( + self, + *, + name: str, + as_type: Literal["tool"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> "LangfuseTool": ... - Args: - name: Name of the span (e.g., function or operation name) - input: Input data for the operation - output: Output data from the operation - metadata: Additional metadata to associate with the span - version: Version identifier for the code or component - level: Importance level of the span (info, warning, error) - status_message: Optional status message for the span + @overload + def start_observation( + self, + *, + name: str, + as_type: Literal["chain"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> "LangfuseChain": ... - Returns: - A context manager that yields a new LangfuseSpan + @overload + def start_observation( + self, + *, + name: str, + as_type: Literal["retriever"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> "LangfuseRetriever": ... - Example: - ```python - with langfuse.start_as_current_span(name="process-request") as parent_span: - # Parent span is active here - - # Create a child span with context management - with parent_span.start_as_current_span(name="validate-input") as child_span: - # Child span is active here - validation_result = validate(request_data) - child_span.update(output=validation_result) - - # Back to parent span context - result = process_validated_data(validation_result) - parent_span.update(output=result) - ``` - """ - return cast( - _AgnosticContextManager["LangfuseSpan"], - self._langfuse_client._create_span_with_parent_context( - name=name, - as_type="span", - remote_parent_span=None, - parent=self._otel_span, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - ), - ) + @overload + def start_observation( + self, + *, + name: str, + as_type: Literal["evaluator"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> "LangfuseEvaluator": ... - def start_generation( + @overload + def start_observation( self, *, name: str, + as_type: Literal["embedding"], input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, @@ -767,89 +835,168 @@ def start_generation( usage_details: Optional[Dict[str, int]] = None, cost_details: Optional[Dict[str, float]] = None, prompt: Optional[PromptClient] = None, - ) -> "LangfuseGeneration": - """Create a new child generation span. + ) -> "LangfuseEmbedding": ... - This method creates a new child generation span with this span as the parent. - Generation spans are specialized for AI/LLM operations and include additional - fields for model information, usage stats, and costs. + @overload + def start_observation( + self, + *, + name: str, + as_type: Literal["guardrail"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> "LangfuseGuardrail": ... - Unlike start_as_current_generation(), this method does not set the new span - as the current span in the context. + @overload + def start_observation( + self, + *, + name: str, + as_type: Literal["event"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> "LangfuseEvent": ... - Args: - name: Name of the generation operation - input: Input data for the model (e.g., prompts) - output: Output from the model (e.g., completions) - metadata: Additional metadata to associate with the generation - version: Version identifier for the model or component - level: Importance level of the generation (info, warning, error) - status_message: Optional status message for the generation - completion_start_time: When the model started generating the response - model: Name/identifier of the AI model used (e.g., "gpt-4") - model_parameters: Parameters used for the model (e.g., temperature, max_tokens) - usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) - cost_details: Cost information for the model call - prompt: Associated prompt template from Langfuse prompt management + def start_observation( + self, + *, + name: str, + as_type: ObservationTypeLiteral = "span", + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + ) -> Union[ + "LangfuseSpan", + "LangfuseGeneration", + "LangfuseAgent", + "LangfuseTool", + "LangfuseChain", + "LangfuseRetriever", + "LangfuseEvaluator", + "LangfuseEmbedding", + "LangfuseGuardrail", + "LangfuseEvent", + ]: + """Create a new child observation of the specified type. + + This is the generic method for creating any type of child observation. + Unlike start_as_current_observation(), this method does not set the new + observation as the current observation in the context. - Returns: - A new LangfuseGeneration that must be ended with .end() when complete + Args: + name: Name of the observation + as_type: Type of observation to create + input: Input data for the operation + output: Output data from the operation + metadata: Additional metadata to associate with the observation + version: Version identifier for the code or component + level: Importance level of the observation (info, warning, error) + status_message: Optional status message for the observation + completion_start_time: When the model started generating (for generation types) + model: Name/identifier of the AI model used (for generation types) + model_parameters: Parameters used for the model (for generation types) + usage_details: Token usage information (for generation types) + cost_details: Cost information (for generation types) + prompt: Associated prompt template (for generation types) - Example: - ```python - span = langfuse.start_span(name="process-query") - try: - # Create a generation child span - generation = span.start_generation( - name="generate-answer", - model="gpt-4", - input={"prompt": "Explain quantum computing"} - ) - try: - # Call model API - response = llm.generate(...) - - generation.update( - output=response.text, - usage_details={ - "prompt_tokens": response.usage.prompt_tokens, - "completion_tokens": response.usage.completion_tokens - } - ) - finally: - generation.end() - - # Continue with parent span - span.update(output={"answer": response.text, "source": "gpt-4"}) - finally: - span.end() - ``` + Returns: + A new observation of the specified type that must be ended with .end() """ + if as_type == "event": + timestamp = time_ns() + event_span = self._langfuse_client._otel_tracer.start_span( + name=name, start_time=timestamp + ) + return cast( + LangfuseEvent, + LangfuseEvent( + otel_span=event_span, + langfuse_client=self._langfuse_client, + input=input, + output=output, + metadata=metadata, + environment=self._environment, + release=self._release, + version=version, + level=level, + status_message=status_message, + ).end(end_time=timestamp), + ) + + observation_class = _OBSERVATION_CLASS_MAP.get(as_type) + if not observation_class: + langfuse_logger.warning( + f"Unknown observation type: {as_type}, falling back to LangfuseSpan" + ) + observation_class = LangfuseSpan + with otel_trace_api.use_span(self._otel_span): new_otel_span = self._langfuse_client._otel_tracer.start_span(name=name) - return LangfuseGeneration( - otel_span=new_otel_span, - langfuse_client=self._langfuse_client, - environment=self._environment, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, - ) + common_args = { + "otel_span": new_otel_span, + "langfuse_client": self._langfuse_client, + "environment": self._environment, + "release": self._release, + "input": input, + "output": output, + "metadata": metadata, + "version": version, + "level": level, + "status_message": status_message, + } + + if as_type in get_observation_types_list(ObservationTypeGenerationLike): + common_args.update( + { + "completion_start_time": completion_start_time, + "model": model, + "model_parameters": model_parameters, + "usage_details": usage_details, + "cost_details": cost_details, + "prompt": prompt, + } + ) + + return observation_class(**common_args) # type: ignore[no-any-return,return-value,arg-type] + + @overload + def start_as_current_observation( + self, + *, + name: str, + as_type: Literal["span"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> _AgnosticContextManager["LangfuseSpan"]: ... - def start_as_current_generation( + @overload + def start_as_current_observation( self, *, name: str, + as_type: Literal["generation"], input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, @@ -862,79 +1009,184 @@ def start_as_current_generation( usage_details: Optional[Dict[str, int]] = None, cost_details: Optional[Dict[str, float]] = None, prompt: Optional[PromptClient] = None, - ) -> _AgnosticContextManager["LangfuseGeneration"]: - """Create a new child generation span and set it as the current span in a context manager. + ) -> _AgnosticContextManager["LangfuseGeneration"]: ... - This method creates a new child generation span and sets it as the current span - within a context manager. Generation spans are specialized for AI/LLM operations - and include additional fields for model information, usage stats, and costs. + @overload + def start_as_current_observation( + self, + *, + name: str, + as_type: Literal["embedding"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + ) -> _AgnosticContextManager["LangfuseEmbedding"]: ... + + @overload + def start_as_current_observation( + self, + *, + name: str, + as_type: Literal["agent"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> _AgnosticContextManager["LangfuseAgent"]: ... + + @overload + def start_as_current_observation( + self, + *, + name: str, + as_type: Literal["tool"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> _AgnosticContextManager["LangfuseTool"]: ... + + @overload + def start_as_current_observation( + self, + *, + name: str, + as_type: Literal["chain"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> _AgnosticContextManager["LangfuseChain"]: ... + + @overload + def start_as_current_observation( + self, + *, + name: str, + as_type: Literal["retriever"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> _AgnosticContextManager["LangfuseRetriever"]: ... + + @overload + def start_as_current_observation( + self, + *, + name: str, + as_type: Literal["evaluator"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> _AgnosticContextManager["LangfuseEvaluator"]: ... + + @overload + def start_as_current_observation( + self, + *, + name: str, + as_type: Literal["guardrail"], + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + ) -> _AgnosticContextManager["LangfuseGuardrail"]: ... + + def start_as_current_observation( # type: ignore[misc] + self, + *, + name: str, + as_type: ObservationTypeLiteralNoEvent = "span", + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + # TODO: or union of context managers? + ) -> _AgnosticContextManager[ + Union[ + "LangfuseSpan", + "LangfuseGeneration", + "LangfuseAgent", + "LangfuseTool", + "LangfuseChain", + "LangfuseRetriever", + "LangfuseEvaluator", + "LangfuseEmbedding", + "LangfuseGuardrail", + ] + ]: + """Create a new child observation and set it as the current observation in a context manager. + + This is the generic method for creating any type of child observation with + context management. It delegates to the client's _create_span_with_parent_context method. Args: - name: Name of the generation operation - input: Input data for the model (e.g., prompts) - output: Output from the model (e.g., completions) - metadata: Additional metadata to associate with the generation - version: Version identifier for the model or component - level: Importance level of the generation (info, warning, error) - status_message: Optional status message for the generation - completion_start_time: When the model started generating the response - model: Name/identifier of the AI model used (e.g., "gpt-4") - model_parameters: Parameters used for the model (e.g., temperature, max_tokens) - usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) - cost_details: Cost information for the model call - prompt: Associated prompt template from Langfuse prompt management + name: Name of the observation + as_type: Type of observation to create + input: Input data for the operation + output: Output data from the operation + metadata: Additional metadata to associate with the observation + version: Version identifier for the code or component + level: Importance level of the observation (info, warning, error) + status_message: Optional status message for the observation + completion_start_time: When the model started generating (for generation types) + model: Name/identifier of the AI model used (for generation types) + model_parameters: Parameters used for the model (for generation types) + usage_details: Token usage information (for generation types) + cost_details: Cost information (for generation types) + prompt: Associated prompt template (for generation types) Returns: - A context manager that yields a new LangfuseGeneration - - Example: - ```python - with langfuse.start_as_current_span(name="process-request") as span: - # Prepare data - query = preprocess_user_query(user_input) - - # Create a generation span with context management - with span.start_as_current_generation( - name="generate-answer", - model="gpt-4", - input={"query": query} - ) as generation: - # Generation span is active here - response = llm.generate(query) - - # Update with results - generation.update( - output=response.text, - usage_details={ - "prompt_tokens": response.usage.prompt_tokens, - "completion_tokens": response.usage.completion_tokens - } - ) - - # Back to parent span context - span.update(output={"answer": response.text, "source": "gpt-4"}) - ``` + A context manager that yields a new observation of the specified type """ - return cast( - _AgnosticContextManager["LangfuseGeneration"], - self._langfuse_client._create_span_with_parent_context( - name=name, - as_type="generation", - remote_parent_span=None, - parent=self._otel_span, - input=input, - output=output, - metadata=metadata, - version=version, - level=level, - status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, - ), + return self._langfuse_client._create_span_with_parent_context( + name=name, + as_type=as_type, + remote_parent_span=None, + parent=self._otel_span, + input=input, + output=output, + metadata=metadata, + version=version, + level=level, + status_message=status_message, + completion_start_time=completion_start_time, + model=model, + model_parameters=model_parameters, + usage_details=usage_details, + cost_details=cost_details, + prompt=prompt, ) def create_event( @@ -983,6 +1235,7 @@ def create_event( output=output, metadata=metadata, environment=self._environment, + release=self._release, version=version, level=level, status_message=status_message, @@ -990,12 +1243,14 @@ def create_event( ) -class LangfuseGeneration(LangfuseSpanWrapper): - """Specialized span implementation for AI model generations in Langfuse. +class LangfuseSpan(LangfuseObservationWrapper): + """Standard span implementation for general operations in Langfuse. - This class represents a generation span specifically designed for tracking - AI/LLM operations. It extends the base LangfuseSpanWrapper with specialized - attributes for model details, token usage, and costs. + This class represents a general-purpose span that can be used to trace + any operation in your application. It extends the base LangfuseObservationWrapper + with specific methods for creating child spans, generations, and updating + span-specific attributes. If possible, use a more specific type for + better observability and insights. """ def __init__( @@ -1007,61 +1262,58 @@ def __init__( output: Optional[Any] = None, metadata: Optional[Any] = None, environment: Optional[str] = None, + release: Optional[str] = None, version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, - completion_start_time: Optional[datetime] = None, - model: Optional[str] = None, - model_parameters: Optional[Dict[str, MapValue]] = None, - usage_details: Optional[Dict[str, int]] = None, - cost_details: Optional[Dict[str, float]] = None, - prompt: Optional[PromptClient] = None, ): - """Initialize a new LangfuseGeneration span. + """Initialize a new LangfuseSpan. Args: otel_span: The OpenTelemetry span to wrap langfuse_client: Reference to the parent Langfuse client - input: Input data for the generation (e.g., prompts) - output: Output from the generation (e.g., completions) - metadata: Additional metadata to associate with the generation + input: Input data for the span (any JSON-serializable object) + output: Output data from the span (any JSON-serializable object) + metadata: Additional metadata to associate with the span environment: The tracing environment - version: Version identifier for the model or component - level: Importance level of the generation (info, warning, error) - status_message: Optional status message for the generation - completion_start_time: When the model started generating the response - model: Name/identifier of the AI model used (e.g., "gpt-4") - model_parameters: Parameters used for the model (e.g., temperature, max_tokens) - usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) - cost_details: Cost information for the model call - prompt: Associated prompt template from Langfuse prompt management + release: Release identifier for the application + version: Version identifier for the code or component + level: Importance level of the span (info, warning, error) + status_message: Optional status message for the span """ super().__init__( otel_span=otel_span, - as_type="generation", + as_type="span", langfuse_client=langfuse_client, input=input, output=output, metadata=metadata, environment=environment, + release=release, version=version, level=level, status_message=status_message, - completion_start_time=completion_start_time, - model=model, - model_parameters=model_parameters, - usage_details=usage_details, - cost_details=cost_details, - prompt=prompt, ) - def update( + +class LangfuseGeneration(LangfuseObservationWrapper): + """Specialized span implementation for AI model generations in Langfuse. + + This class represents a generation span specifically designed for tracking + AI/LLM operations. It extends the base LangfuseObservationWrapper with specialized + attributes for model details, token usage, and costs. + """ + + def __init__( self, *, - name: Optional[str] = None, + otel_span: otel_trace_api.Span, + langfuse_client: "Langfuse", input: Optional[Any] = None, output: Optional[Any] = None, metadata: Optional[Any] = None, + environment: Optional[str] = None, + release: Optional[str] = None, version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, @@ -1071,19 +1323,17 @@ def update( usage_details: Optional[Dict[str, int]] = None, cost_details: Optional[Dict[str, float]] = None, prompt: Optional[PromptClient] = None, - **kwargs: Dict[str, Any], - ) -> "LangfuseGeneration": - """Update this generation span with new information. - - This method updates the generation span with new information that becomes - available during or after the model generation, such as model outputs, - token usage statistics, or cost details. + ): + """Initialize a new LangfuseGeneration span. Args: - name: The generation name - input: Updated input data for the model - output: Output from the model (e.g., completions) + otel_span: The OpenTelemetry span to wrap + langfuse_client: Reference to the parent Langfuse client + input: Input data for the generation (e.g., prompts) + output: Output from the generation (e.g., completions) metadata: Additional metadata to associate with the generation + environment: The tracing environment + release: Release identifier for the application version: Version identifier for the model or component level: Importance level of the generation (info, warning, error) status_message: Optional status message for the generation @@ -1093,55 +1343,16 @@ def update( usage_details: Token usage information (e.g., prompt_tokens, completion_tokens) cost_details: Cost information for the model call prompt: Associated prompt template from Langfuse prompt management - **kwargs: Additional keyword arguments (ignored) - - Example: - ```python - generation = langfuse.start_generation( - name="answer-generation", - model="gpt-4", - input={"prompt": "Explain quantum computing"} - ) - try: - # Call model API - response = llm.generate(...) - - # Update with results - generation.update( - output=response.text, - usage_details={ - "prompt_tokens": response.usage.prompt_tokens, - "completion_tokens": response.usage.completion_tokens, - "total_tokens": response.usage.total_tokens - }, - cost_details={ - "total_cost": 0.0035 - } - ) - finally: - generation.end() - ``` """ - if not self._otel_span.is_recording(): - return self - - processed_input = self._process_media_and_apply_mask( - data=input, field="input", span=self._otel_span - ) - processed_output = self._process_media_and_apply_mask( - data=output, field="output", span=self._otel_span - ) - processed_metadata = self._process_media_and_apply_mask( - data=metadata, field="metadata", span=self._otel_span - ) - - if name: - self._otel_span.update_name(name) - - attributes = create_generation_attributes( - input=processed_input, - output=processed_output, - metadata=processed_metadata, + super().__init__( + as_type="generation", + otel_span=otel_span, + langfuse_client=langfuse_client, + input=input, + output=output, + metadata=metadata, + environment=environment, + release=release, version=version, level=level, status_message=status_message, @@ -1153,12 +1364,8 @@ def update( prompt=prompt, ) - self._otel_span.set_attributes(attributes=attributes) - - return self - -class LangfuseEvent(LangfuseSpanWrapper): +class LangfuseEvent(LangfuseObservationWrapper): """Specialized span implementation for Langfuse Events.""" def __init__( @@ -1170,6 +1377,7 @@ def __init__( output: Optional[Any] = None, metadata: Optional[Any] = None, environment: Optional[str] = None, + release: Optional[str] = None, version: Optional[str] = None, level: Optional[SpanLevel] = None, status_message: Optional[str] = None, @@ -1183,6 +1391,7 @@ def __init__( output: Output from the event metadata: Additional metadata to associate with the generation environment: The tracing environment + release: Release identifier for the application version: Version identifier for the model or component level: Importance level of the generation (info, warning, error) status_message: Optional status message for the generation @@ -1195,7 +1404,116 @@ def __init__( output=output, metadata=metadata, environment=environment, + release=release, version=version, level=level, status_message=status_message, ) + + def update( + self, + *, + name: Optional[str] = None, + input: Optional[Any] = None, + output: Optional[Any] = None, + metadata: Optional[Any] = None, + version: Optional[str] = None, + level: Optional[SpanLevel] = None, + status_message: Optional[str] = None, + completion_start_time: Optional[datetime] = None, + model: Optional[str] = None, + model_parameters: Optional[Dict[str, MapValue]] = None, + usage_details: Optional[Dict[str, int]] = None, + cost_details: Optional[Dict[str, float]] = None, + prompt: Optional[PromptClient] = None, + **kwargs: Any, + ) -> "LangfuseEvent": + """Update is not allowed for LangfuseEvent because events cannot be updated. + + This method logs a warning and returns self without making changes. + + Returns: + self: Returns the unchanged LangfuseEvent instance + """ + langfuse_logger.warning( + "Attempted to update LangfuseEvent observation. Events cannot be updated after creation." + ) + return self + + +class LangfuseAgent(LangfuseObservationWrapper): + """Agent observation for reasoning blocks that act on tools using LLM guidance.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize a new LangfuseAgent span.""" + kwargs["as_type"] = "agent" + super().__init__(**kwargs) + + +class LangfuseTool(LangfuseObservationWrapper): + """Tool observation representing external tool calls, e.g., calling a weather API.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize a new LangfuseTool span.""" + kwargs["as_type"] = "tool" + super().__init__(**kwargs) + + +class LangfuseChain(LangfuseObservationWrapper): + """Chain observation for connecting LLM application steps, e.g. passing context from retriever to LLM.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize a new LangfuseChain span.""" + kwargs["as_type"] = "chain" + super().__init__(**kwargs) + + +class LangfuseRetriever(LangfuseObservationWrapper): + """Retriever observation for data retrieval steps, e.g. vector store or database queries.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize a new LangfuseRetriever span.""" + kwargs["as_type"] = "retriever" + super().__init__(**kwargs) + + +class LangfuseEmbedding(LangfuseObservationWrapper): + """Embedding observation for LLM embedding calls, typically used before retrieval.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize a new LangfuseEmbedding span.""" + kwargs["as_type"] = "embedding" + super().__init__(**kwargs) + + +class LangfuseEvaluator(LangfuseObservationWrapper): + """Evaluator observation for assessing relevance, correctness, or helpfulness of LLM outputs.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize a new LangfuseEvaluator span.""" + kwargs["as_type"] = "evaluator" + super().__init__(**kwargs) + + +class LangfuseGuardrail(LangfuseObservationWrapper): + """Guardrail observation for protection e.g. against jailbreaks or offensive content.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize a new LangfuseGuardrail span.""" + kwargs["as_type"] = "guardrail" + super().__init__(**kwargs) + + +_OBSERVATION_CLASS_MAP.update( + { + "span": LangfuseSpan, + "generation": LangfuseGeneration, + "agent": LangfuseAgent, + "tool": LangfuseTool, + "chain": LangfuseChain, + "retriever": LangfuseRetriever, + "evaluator": LangfuseEvaluator, + "embedding": LangfuseEmbedding, + "guardrail": LangfuseGuardrail, + } +) diff --git a/langfuse/_client/span_filter.py b/langfuse/_client/span_filter.py new file mode 100644 index 000000000..8ac384740 --- /dev/null +++ b/langfuse/_client/span_filter.py @@ -0,0 +1,101 @@ +"""Span filter predicates for controlling OpenTelemetry span export. + +This module provides composable filter functions that determine which spans +the LangfuseSpanProcessor forwards to the Langfuse backend. +""" + +from opentelemetry.sdk.trace import ReadableSpan + +from langfuse._client.constants import LANGFUSE_TRACER_NAME + +KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES = frozenset( + { + LANGFUSE_TRACER_NAME, + "agent_framework", + "autogen-core", + "ai", + "haystack", + "langsmith", + "litellm", + "openinference", + "opentelemetry.instrumentation.agno", + "opentelemetry.instrumentation.alephalpha", + "opentelemetry.instrumentation.anthropic", + "opentelemetry.instrumentation.bedrock", + "opentelemetry.instrumentation.cohere", + "opentelemetry.instrumentation.crewai", + "opentelemetry.instrumentation.google_generativeai", + "opentelemetry.instrumentation.groq", + "opentelemetry.instrumentation.haystack", + "opentelemetry.instrumentation.langchain", + "opentelemetry.instrumentation.llamaindex", + "opentelemetry.instrumentation.mistralai", + "opentelemetry.instrumentation.ollama", + "opentelemetry.instrumentation.openai", + "opentelemetry.instrumentation.openai_agents", + "opentelemetry.instrumentation.openai_v2", + "opentelemetry.instrumentation.replicate", + "opentelemetry.instrumentation.sagemaker", + "opentelemetry.instrumentation.together", + "opentelemetry.instrumentation.transformers", + "opentelemetry.instrumentation.vertexai", + "opentelemetry.instrumentation.voyageai", + "opentelemetry.instrumentation.watsonx", + "opentelemetry.instrumentation.writer", + "pydantic-ai", + "strands-agents", + "vllm", + } +) +"""Known instrumentation scope namespace prefixes. + +Prefix matching is boundary-aware: +- exact match (``scope == prefix``) +- direct descendant scopes (``scope.startswith(prefix + ".")``) + +Please create a GitHub issue in https://github.com/langfuse/langfuse if you'd like to expand this default allow list. +""" + + +def is_langfuse_span(span: ReadableSpan) -> bool: + """Return whether the span was created by the Langfuse SDK tracer.""" + return ( + span.instrumentation_scope is not None + and span.instrumentation_scope.name == LANGFUSE_TRACER_NAME + ) + + +def is_genai_span(span: ReadableSpan) -> bool: + """Return whether the span has any ``gen_ai.*`` semantic convention attribute.""" + if span.attributes is None: + return False + + return any( + isinstance(key, str) and key.startswith("gen_ai") + for key in span.attributes.keys() + ) + + +def _matches_scope_prefix(scope_name: str, prefix: str) -> bool: + """Return whether a scope matches a prefix using namespace boundaries.""" + return scope_name == prefix or scope_name.startswith(f"{prefix}.") + + +def is_known_llm_instrumentor(span: ReadableSpan) -> bool: + """Return whether the span comes from a known LLM instrumentation scope.""" + if span.instrumentation_scope is None: + return False + + scope_name = span.instrumentation_scope.name + + return any( + _matches_scope_prefix(scope_name, prefix) + for prefix in KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES + ) + + +def is_default_export_span(span: ReadableSpan) -> bool: + """Return whether a span should be exported by default.""" + return ( + is_langfuse_span(span) or is_genai_span(span) or is_known_llm_instrumentor(span) + ) diff --git a/langfuse/_client/span_processor.py b/langfuse/_client/span_processor.py index ca8fb9b5a..42a1d2cf7 100644 --- a/langfuse/_client/span_processor.py +++ b/langfuse/_client/span_processor.py @@ -13,20 +13,30 @@ import base64 import os -from typing import Dict, List, Optional +import threading +from typing import Callable, Dict, List, Optional, cast +from opentelemetry import context as context_api +from opentelemetry.context import Context from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.trace import ReadableSpan -from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.sdk.trace import ReadableSpan, Span +from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter +from opentelemetry.trace import format_span_id, format_trace_id -from langfuse._client.constants import LANGFUSE_TRACER_NAME +from langfuse._client.attributes import LangfuseOtelSpanAttributes from langfuse._client.environment_variables import ( LANGFUSE_FLUSH_AT, LANGFUSE_FLUSH_INTERVAL, + LANGFUSE_OTEL_TRACES_EXPORT_PATH, ) +from langfuse._client.propagation import ( + _get_langfuse_trace_id_from_baggage, + _get_propagated_attributes_from_context, +) +from langfuse._client.span_filter import is_default_export_span, is_langfuse_span from langfuse._client.utils import span_formatter +from langfuse._version import __version__ as langfuse_version from langfuse.logger import langfuse_logger -from langfuse.version import __version__ as langfuse_version class LangfuseSpanProcessor(BatchSpanProcessor): @@ -51,12 +61,14 @@ def __init__( *, public_key: str, secret_key: str, - host: str, + base_url: str, timeout: Optional[int] = None, flush_at: Optional[int] = None, flush_interval: Optional[float] = None, blocked_instrumentation_scopes: Optional[List[str]] = None, + should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, additional_headers: Optional[Dict[str, str]] = None, + span_exporter: Optional[SpanExporter] = None, ): self.public_key = public_key self.blocked_instrumentation_scopes = ( @@ -64,6 +76,10 @@ def __init__( if blocked_instrumentation_scopes is not None else [] ) + self._should_export_span = should_export_span or is_default_export_span + + self._app_root_lock = threading.Lock() + self._span_export_expectation_by_id: Dict[str, bool] = {} env_flush_at = os.environ.get(LANGFUSE_FLUSH_AT, None) flush_at = flush_at or int(env_flush_at) if env_flush_at is not None else None @@ -75,29 +91,38 @@ def __init__( else None ) - basic_auth_header = "Basic " + base64.b64encode( - f"{public_key}:{secret_key}".encode("utf-8") - ).decode("ascii") - - # Prepare default headers - default_headers = { - "Authorization": basic_auth_header, - "x_langfuse_sdk_name": "python", - "x_langfuse_sdk_version": langfuse_version, - "x_langfuse_public_key": public_key, - } - - # Merge additional headers if provided - headers = {**default_headers, **(additional_headers or {})} - - langfuse_span_exporter = OTLPSpanExporter( - endpoint=f"{host}/api/public/otel/v1/traces", - headers=headers, - timeout=timeout, - ) + if span_exporter is None: + basic_auth_header = "Basic " + base64.b64encode( + f"{public_key}:{secret_key}".encode("utf-8") + ).decode("ascii") + + # Prepare default headers + default_headers = { + "Authorization": basic_auth_header, + "x-langfuse-sdk-name": "python", + "x-langfuse-sdk-version": langfuse_version, + "x-langfuse-public-key": public_key, + } + + # Merge additional headers if provided + headers = {**default_headers, **(additional_headers or {})} + + traces_export_path = os.environ.get(LANGFUSE_OTEL_TRACES_EXPORT_PATH, None) + + endpoint = ( + f"{base_url}/{traces_export_path}" + if traces_export_path + else f"{base_url}/api/public/otel/v1/traces" + ) + + span_exporter = OTLPSpanExporter( + endpoint=endpoint, + headers=headers, + timeout=timeout, + ) super().__init__( - span_exporter=langfuse_span_exporter, + span_exporter=span_exporter, export_timeout_millis=timeout * 1_000 if timeout else None, max_export_batch_size=flush_at, schedule_delay_millis=flush_interval * 1_000 @@ -105,32 +130,129 @@ def __init__( else None, ) + def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None: + context = parent_context or context_api.get_current() + propagated_attributes = _get_propagated_attributes_from_context(context) + + if propagated_attributes: + span.set_attributes(propagated_attributes) + + langfuse_logger.debug( + f"Propagated {len(propagated_attributes)} attributes to span '{format_span_id(span.context.span_id)}': {propagated_attributes}" + ) + + try: + self._mark_app_root_candidate(span=span, parent_context=context) + except Exception as error: + langfuse_logger.debug( + "Trace: app-root start-time check failed. Span will not be marked as app root | " + f"span_name='{getattr(span, 'name', '')}' | " + f"Error: {error}" + ) + + return super().on_start(span, parent_context) + def on_end(self, span: ReadableSpan) -> None: - # Only export spans that belong to the scoped project - # This is important to not send spans to wrong project in multi-project setups - if self._is_langfuse_span(span) and not self._is_langfuse_project_span(span): + try: + # Only export spans that belong to the scoped project + # This is important to not send spans to wrong project in multi-project setups + if is_langfuse_span(span) and not self._is_langfuse_project_span(span): + langfuse_logger.debug( + f"Security: Span rejected - belongs to project '{span.instrumentation_scope.attributes.get('public_key') if span.instrumentation_scope and span.instrumentation_scope.attributes else None}' but processor is for '{self.public_key}'. " + f"This prevents cross-project data leakage in multi-project environments." + ) + return + + # Do not export spans from blocked instrumentation scopes + if self._is_blocked_instrumentation_scope(span): + langfuse_logger.debug( + "Trace: Dropping span due to blocked instrumentation scope | " + f"span_name='{span.name}' | " + f"instrumentation_scope='{self._get_scope_name(span)}'" + ) + return + + # Apply custom or default span filter + try: + should_export = self._should_export_span(span) + except Exception as error: + langfuse_logger.error( + "Trace: should_export_span callback raised an error. " + f"Dropping span name='{span.name}' scope='{self._get_scope_name(span)}'. " + f"Error: {error}" + ) + return + + if not should_export: + langfuse_logger.debug( + "Trace: Dropping span due to should_export_span filter | " + f"span_name='{span.name}' | " + f"instrumentation_scope='{self._get_scope_name(span)}'" + ) + return + langfuse_logger.debug( - f"Security: Span rejected - belongs to project '{span.instrumentation_scope.attributes.get('public_key') if span.instrumentation_scope and span.instrumentation_scope.attributes else None}' but processor is for '{self.public_key}'. " - f"This prevents cross-project data leakage in multi-project environments." + f"Trace: Processing span name='{span._name}' | Full details:\n{span_formatter(span)}" ) - return - # Do not export spans from blocked instrumentation scopes - if self._is_blocked_instrumentation_scope(span): - return + super().on_end(span) + finally: + self._cleanup_app_root_state(span) - langfuse_logger.debug( - f"Trace: Processing span name='{span._name}' | Full details:\n{span_formatter(span)}" - ) + def _mark_app_root_candidate(self, *, span: Span, parent_context: Context) -> None: + trace_id = format_trace_id(span.context.trace_id) + span_id = format_span_id(span.context.span_id) + parent_span_id = format_span_id(span.parent.span_id) if span.parent else None + expected_exported = self._is_expected_exported_at_start(span) + propagated_trace_id = _get_langfuse_trace_id_from_baggage(parent_context) - super().on_end(span) + with self._app_root_lock: + parent_expected_exported = ( + parent_span_id is not None + and self._span_export_expectation_by_id.get(parent_span_id) is True + ) + suppressed_by_parent_claim = propagated_trace_id == trace_id - @staticmethod - def _is_langfuse_span(span: ReadableSpan) -> bool: - return ( - span.instrumentation_scope is not None - and span.instrumentation_scope.name == LANGFUSE_TRACER_NAME - ) + self._span_export_expectation_by_id[span_id] = expected_exported + + mark_app_root = ( + expected_exported + and not parent_expected_exported + and not suppressed_by_parent_claim + ) + + if mark_app_root: + span.set_attribute(LangfuseOtelSpanAttributes.IS_APP_ROOT, True) + + def _cleanup_app_root_state(self, span: ReadableSpan) -> None: + span_id = format_span_id(span.context.span_id) + + with self._app_root_lock: + self._span_export_expectation_by_id.pop(span_id, None) + + def _is_expected_exported_at_start(self, span: Span) -> bool: + readable_span = cast(ReadableSpan, span) + + if is_langfuse_span(readable_span) and not self._is_langfuse_project_span( + readable_span + ): + return False + + if self._is_blocked_instrumentation_scope(readable_span): + return False + + try: + return bool(self._should_export_span(readable_span)) + except Exception as error: + langfuse_logger.debug( + "Trace: should_export_span callback raised during app-root " + f"start-time check. Span will not be marked as app root | " + f"span_name='{readable_span.name}' | " + f"instrumentation_scope='{self._get_scope_name(readable_span)}' | " + f"Error: {error}" + ) + + return False def _is_blocked_instrumentation_scope(self, span: ReadableSpan) -> bool: return ( @@ -139,7 +261,7 @@ def _is_blocked_instrumentation_scope(self, span: ReadableSpan) -> bool: ) def _is_langfuse_project_span(self, span: ReadableSpan) -> bool: - if not LangfuseSpanProcessor._is_langfuse_span(span): + if not is_langfuse_span(span): return False if span.instrumentation_scope is not None: @@ -152,3 +274,10 @@ def _is_langfuse_project_span(self, span: ReadableSpan) -> bool: return public_key_on_span == self.public_key return False + + @staticmethod + def _get_scope_name(span: ReadableSpan) -> Optional[str]: + if span.instrumentation_scope is None: + return None + + return span.instrumentation_scope.name diff --git a/langfuse/_client/utils.py b/langfuse/_client/utils.py index dac7a3f1b..ccce3d9ef 100644 --- a/langfuse/_client/utils.py +++ b/langfuse/_client/utils.py @@ -1,10 +1,15 @@ """Utility functions for Langfuse OpenTelemetry integration. This module provides utility functions for working with OpenTelemetry spans, -including formatting and serialization of span data. +including formatting and serialization of span data, and async execution helpers. """ +import asyncio +import contextvars import json +import threading +from hashlib import sha256 +from typing import Any, Coroutine from opentelemetry import trace as otel_trace_api from opentelemetry.sdk import util @@ -58,3 +63,72 @@ def span_formatter(span: ReadableSpan) -> str: ) + "\n" ) + + +class _RunAsyncThread(threading.Thread): + """Helper thread class for running async coroutines in a separate thread.""" + + def __init__(self, coro: Coroutine[Any, Any, Any]) -> None: + self.coro = coro + self.context = contextvars.copy_context() + self.result: Any = None + self.exception: Exception | None = None + super().__init__() + + def run(self) -> None: + try: + self.result = self.context.run(asyncio.run, self.coro) + except Exception as e: + self.exception = e + + +def run_async_safely(coro: Coroutine[Any, Any, Any]) -> Any: + """Safely run an async coroutine, handling existing event loops. + + This function detects if there's already a running event loop and uses + a separate thread if needed to avoid the "asyncio.run() cannot be called + from a running event loop" error. This is particularly useful in environments + like Jupyter notebooks, FastAPI applications, or other async frameworks. + + Args: + coro: The coroutine to run + + Returns: + The result of the coroutine + + Raises: + Any exception raised by the coroutine + + Example: + ```python + # Works in both sync and async contexts + async def my_async_function(): + await asyncio.sleep(1) + return "done" + + result = run_async_safely(my_async_function()) + ``` + """ + try: + # Check if there's already a running event loop + loop = asyncio.get_running_loop() + except RuntimeError: + # No running loop, safe to use asyncio.run() + return asyncio.run(coro) + + if loop and loop.is_running(): + # There's a running loop, use a separate thread + thread = _RunAsyncThread(coro) + thread.start() + thread.join() + + if thread.exception: + raise thread.exception + return thread.result + else: + # Loop exists but not running, safe to use asyncio.run() + return asyncio.run(coro) + + +def get_sha256_hash_hex(value: Any) -> str: + return sha256(value.encode("utf-8")).digest().hex() diff --git a/langfuse/_task_manager/media_manager.py b/langfuse/_task_manager/media_manager.py index 13fa5f0c6..7a7123798 100644 --- a/langfuse/_task_manager/media_manager.py +++ b/langfuse/_task_manager/media_manager.py @@ -1,38 +1,37 @@ -import logging import os import time from queue import Empty, Full, Queue from typing import Any, Callable, Optional, TypeVar, cast import backoff -import requests +import httpx from typing_extensions import ParamSpec from langfuse._client.environment_variables import LANGFUSE_MEDIA_UPLOAD_ENABLED from langfuse._utils import _get_timestamp -from langfuse.api import GetMediaUploadUrlRequest, PatchMediaBody -from langfuse.api.client import FernLangfuse +from langfuse.api import LangfuseAPI, MediaContentType from langfuse.api.core import ApiError -from langfuse.api.resources.media.types.media_content_type import MediaContentType +from langfuse.logger import langfuse_logger as logger from langfuse.media import LangfuseMedia from .media_upload_queue import UploadMediaJob T = TypeVar("T") P = ParamSpec("P") +_SHUTDOWN_SENTINEL = object() class MediaManager: - _log = logging.getLogger("langfuse") - def __init__( self, *, - api_client: FernLangfuse, + api_client: LangfuseAPI, + httpx_client: httpx.Client, media_upload_queue: Queue, max_retries: Optional[int] = 3, ): self._api_client = api_client + self._httpx_client = httpx_client self._queue = media_upload_queue self._max_retries = max_retries self._enabled = os.environ.get( @@ -42,21 +41,34 @@ def __init__( def process_next_media_upload(self) -> None: try: upload_job = self._queue.get(block=True, timeout=1) - self._log.debug( + + if upload_job is _SHUTDOWN_SENTINEL: + self._queue.task_done() + return + + logger.debug( f"Media: Processing upload for media_id={upload_job['media_id']} in trace_id={upload_job['trace_id']}" ) self._process_upload_media_job(data=upload_job) self._queue.task_done() except Empty: - self._log.debug("Queue: Media upload queue is empty, waiting for new jobs") pass except Exception as e: - self._log.error( + logger.error( f"Media upload error: Failed to upload media due to unexpected error. Queue item marked as done. Error: {e}" ) self._queue.task_done() + def signal_shutdown(self, *, count: int = 1) -> None: + for _ in range(count): + try: + self._queue.put(_SHUTDOWN_SENTINEL, block=False) + except Full: + # If the queue is full, the consumer will keep draining work and + # observe the paused flag on the next loop iteration. + break + def _find_and_process_media( self, *, @@ -87,7 +99,12 @@ def _process_data_recursively(data: Any, level: int) -> Any: return data - if isinstance(data, str) and data.startswith("data:"): + if ( + isinstance(data, str) + and data.startswith("data:") + and "," in data + and data.split(",", 1)[0].endswith(";base64") + ): media = LangfuseMedia( obj=data, base64_data_uri=data, @@ -180,7 +197,7 @@ def _process_media( return if media._media_id is None: - self._log.error("Media ID is None. Skipping upload.") + logger.error("Media ID is None. Skipping upload.") return try: @@ -199,17 +216,17 @@ def _process_media( item=upload_media_job, block=False, ) - self._log.debug( + logger.debug( f"Queue: Enqueued media ID {media._media_id} for upload processing | trace_id={trace_id} | field={field}" ) except Full: - self._log.warning( + logger.warning( f"Queue capacity: Media queue is full. Failed to process media_id={media._media_id} for trace_id={trace_id}. Consider increasing queue capacity." ) except Exception as e: - self._log.error( + logger.error( f"Media processing error: Failed to process media_id={media._media_id} for trace_id={trace_id}. Error: {str(e)}" ) @@ -220,57 +237,81 @@ def _process_upload_media_job( ) -> None: upload_url_response = self._request_with_backoff( self._api_client.media.get_upload_url, - request=GetMediaUploadUrlRequest( - contentLength=data["content_length"], - contentType=cast(MediaContentType, data["content_type"]), - sha256Hash=data["content_sha256_hash"], - field=data["field"], - traceId=data["trace_id"], - observationId=data["observation_id"], - ), + content_length=data["content_length"], + content_type=cast(MediaContentType, data["content_type"]), + sha256hash=data["content_sha256_hash"], + field=data["field"], + trace_id=data["trace_id"], + observation_id=data["observation_id"], ) upload_url = upload_url_response.upload_url if not upload_url: - self._log.debug( + logger.debug( f"Media status: Media with ID {data['media_id']} already uploaded. Skipping duplicate upload." ) return if upload_url_response.media_id != data["media_id"]: - self._log.error( + logger.error( f"Media integrity error: Media ID mismatch between SDK ({data['media_id']}) and Server ({upload_url_response.media_id}). Upload cancelled. Please check media ID generation logic." ) return + headers = {"Content-Type": data["content_type"]} + + # In self-hosted setups with GCP, do not add unsupported headers that fail the upload + is_self_hosted_gcs_bucket = "storage.googleapis.com" in upload_url + + if not is_self_hosted_gcs_bucket: + headers["x-ms-blob-type"] = "BlockBlob" + headers["x-amz-checksum-sha256"] = data["content_sha256_hash"] + + def _upload_with_status_check() -> httpx.Response: + response = self._httpx_client.put( + upload_url, + headers=headers, + content=data["content_bytes"], + ) + response.raise_for_status() + + return response + upload_start_time = time.time() - upload_response = self._request_with_backoff( - requests.put, - upload_url, - headers={ - "Content-Type": data["content_type"], - "x-amz-checksum-sha256": data["content_sha256_hash"], - "x-ms-blob-type": "BlockBlob", - }, - data=data["content_bytes"], - ) + + try: + upload_response = self._request_with_backoff(_upload_with_status_check) + except httpx.HTTPStatusError as e: + upload_time_ms = int((time.time() - upload_start_time) * 1000) + failed_response = e.response + + if failed_response is not None: + self._request_with_backoff( + self._api_client.media.patch, + media_id=data["media_id"], + uploaded_at=_get_timestamp(), + upload_http_status=failed_response.status_code, + upload_http_error=failed_response.text, + upload_time_ms=upload_time_ms, + ) + + raise + upload_time_ms = int((time.time() - upload_start_time) * 1000) self._request_with_backoff( self._api_client.media.patch, media_id=data["media_id"], - request=PatchMediaBody( - uploadedAt=_get_timestamp(), - uploadHttpStatus=upload_response.status_code, - uploadHttpError=upload_response.text, - uploadTimeMs=upload_time_ms, - ), + uploaded_at=_get_timestamp(), + upload_http_status=upload_response.status_code, + upload_http_error=upload_response.text, + upload_time_ms=upload_time_ms, ) - self._log.debug( + logger.debug( f"Media upload: Successfully uploaded media_id={data['media_id']} for trace_id={data['trace_id']} | status_code={upload_response.status_code} | duration={upload_time_ms}ms | size={data['content_length']} bytes" ) @@ -284,7 +325,7 @@ def _should_give_up(e: Exception) -> bool: and 400 <= e.status_code < 500 and e.status_code != 429 ) - if isinstance(e, requests.exceptions.RequestException): + if isinstance(e, httpx.HTTPStatusError): return ( e.response is not None and e.response.status_code < 500 diff --git a/langfuse/_task_manager/media_upload_consumer.py b/langfuse/_task_manager/media_upload_consumer.py index 182170864..b9058066b 100644 --- a/langfuse/_task_manager/media_upload_consumer.py +++ b/langfuse/_task_manager/media_upload_consumer.py @@ -1,11 +1,11 @@ -import logging import threading +from langfuse.logger import langfuse_logger as logger + from .media_manager import MediaManager class MediaUploadConsumer(threading.Thread): - _log = logging.getLogger("langfuse") _identifier: int _max_retries: int _media_manager: MediaManager @@ -30,7 +30,7 @@ def __init__( def run(self) -> None: """Run the media upload consumer.""" - self._log.debug( + logger.debug( f"Thread: Media upload consumer thread #{self._identifier} started and actively processing queue items" ) while self.running: @@ -38,7 +38,7 @@ def run(self) -> None: def pause(self) -> None: """Pause the media upload consumer.""" - self._log.debug( + logger.debug( f"Thread: Pausing media upload consumer thread #{self._identifier}" ) self.running = False diff --git a/langfuse/_task_manager/score_ingestion_consumer.py b/langfuse/_task_manager/score_ingestion_consumer.py index 1a5b61f91..ea5c2b34e 100644 --- a/langfuse/_task_manager/score_ingestion_consumer.py +++ b/langfuse/_task_manager/score_ingestion_consumer.py @@ -1,29 +1,26 @@ import json -import logging import os import threading import time -from queue import Empty, Queue +from queue import Empty, Full, Queue from typing import Any, List, Optional import backoff - -from ..version import __version__ as langfuse_version - -try: - import pydantic.v1 as pydantic -except ImportError: - import pydantic # type: ignore +from pydantic import BaseModel from langfuse._utils.parse_error import handle_exception from langfuse._utils.request import APIError, LangfuseClient from langfuse._utils.serializer import EventSerializer +from langfuse.logger import langfuse_logger as logger + +from .._version import __version__ as langfuse_version MAX_EVENT_SIZE_BYTES = int(os.environ.get("LANGFUSE_MAX_EVENT_SIZE_BYTES", 1_000_000)) MAX_BATCH_SIZE_BYTES = int(os.environ.get("LANGFUSE_MAX_BATCH_SIZE_BYTES", 2_500_000)) +_SHUTDOWN_SENTINEL = object() -class ScoreIngestionMetadata(pydantic.BaseModel): +class ScoreIngestionMetadata(BaseModel): batch_size: int sdk_name: str sdk_version: str @@ -31,8 +28,6 @@ class ScoreIngestionMetadata(pydantic.BaseModel): class ScoreIngestionConsumer(threading.Thread): - _log = logging.getLogger("langfuse") - def __init__( self, *, @@ -77,9 +72,13 @@ def _next(self) -> list: block=True, timeout=self._flush_interval - elapsed ) + if event is _SHUTDOWN_SENTINEL: + self._ingestion_queue.task_done() + break + # convert pydantic models to dicts - if "body" in event and isinstance(event["body"], pydantic.BaseModel): - event["body"] = event["body"].dict(exclude_none=True) + if "body" in event and isinstance(event["body"], BaseModel): + event["body"] = event["body"].model_dump(exclude_none=True) item_size = self._get_item_size(event) @@ -87,7 +86,7 @@ def _next(self) -> list: try: json.dumps(event, cls=EventSerializer) except Exception as e: - self._log.error( + logger.error( f"Data error: Failed to serialize score object for ingestion. Score will be dropped. Error: {e}" ) self._ingestion_queue.task_done() @@ -98,7 +97,7 @@ def _next(self) -> list: total_size += item_size if total_size >= MAX_BATCH_SIZE_BYTES: - self._log.debug( + logger.debug( f"Batch management: Reached maximum batch size limit ({total_size} bytes). Processing {len(events)} events now." ) break @@ -107,7 +106,7 @@ def _next(self) -> list: break except Exception as e: - self._log.warning( + logger.warning( f"Data processing error: Failed to process score event in consumer thread #{self._identifier}. Event will be dropped. Error: {str(e)}", exc_info=True, ) @@ -121,7 +120,7 @@ def _get_item_size(self, item: Any) -> int: def run(self) -> None: """Run the consumer.""" - self._log.debug( + logger.debug( f"Startup: Score ingestion consumer thread #{self._identifier} started with batch size {self._flush_at} and interval {self._flush_interval}s" ) while self.running: @@ -145,9 +144,15 @@ def upload(self) -> None: def pause(self) -> None: """Pause the consumer.""" self.running = False + try: + self._ingestion_queue.put(_SHUTDOWN_SENTINEL, block=False) + except Full: + # If the queue is full, the consumer will wake up naturally while + # draining items, so a dedicated shutdown signal is not required. + pass def _upload_batch(self, batch: List[Any]) -> None: - self._log.debug( + logger.debug( f"API: Uploading batch of {len(batch)} score events to Langfuse API" ) @@ -156,7 +161,7 @@ def _upload_batch(self, batch: List[Any]) -> None: sdk_name="python", sdk_version=langfuse_version, public_key=self._public_key, - ).dict() + ).model_dump() @backoff.on_exception( backoff.expo, Exception, max_tries=self._max_retries, logger=None @@ -175,6 +180,6 @@ def execute_task_with_backoff(batch: List[Any]) -> None: raise e execute_task_with_backoff(batch) - self._log.debug( + logger.debug( f"API: Successfully sent {len(batch)} score events to Langfuse API in batch mode" ) diff --git a/langfuse/_utils/__init__.py b/langfuse/_utils/__init__.py index e8a02abc1..5be7655c1 100644 --- a/langfuse/_utils/__init__.py +++ b/langfuse/_utils/__init__.py @@ -1,13 +1,10 @@ """@private""" -import logging import typing from datetime import datetime, timezone from langfuse.model import PromptClient -log = logging.getLogger("langfuse") - def _get_timestamp() -> datetime: return datetime.now(timezone.utc) diff --git a/langfuse/_utils/error_logging.py b/langfuse/_utils/error_logging.py index e5a7fe67c..4e3abac92 100644 --- a/langfuse/_utils/error_logging.py +++ b/langfuse/_utils/error_logging.py @@ -1,8 +1,7 @@ import functools -import logging from typing import Any, Callable, List, Optional -logger = logging.getLogger("langfuse") +from langfuse.logger import langfuse_logger as logger def catch_and_log_errors(func: Callable[..., Any]) -> Callable[..., Any]: diff --git a/langfuse/_utils/parse_error.py b/langfuse/_utils/parse_error.py index cb5749f93..2b9a7bd6f 100644 --- a/langfuse/_utils/parse_error.py +++ b/langfuse/_utils/parse_error.py @@ -1,19 +1,19 @@ -import logging from typing import Union # our own api errors from langfuse._utils.request import APIError, APIErrors -from langfuse.api.core import ApiError # fern api errors -from langfuse.api.resources.commons.errors import ( +from langfuse.api import ( AccessDeniedError, Error, MethodNotAllowedError, NotFoundError, + ServiceUnavailableError, UnauthorizedError, ) -from langfuse.api.resources.health.errors import ServiceUnavailableError +from langfuse.api.core import ApiError +from langfuse.logger import langfuse_logger as logger SUPPORT_URL = "https://langfuse.com/support" API_DOCS_URL = "https://api.reference.langfuse.com" @@ -67,10 +67,9 @@ def generate_error_message_fern(error: Error) -> str: def handle_fern_exception(exception: Error) -> None: - log = logging.getLogger("langfuse") - log.debug(exception) + logger.debug(exception) error_message = generate_error_message_fern(exception) - log.error(error_message) + logger.error(error_message) def generate_error_message(exception: Union[APIError, APIErrors, Exception]) -> str: @@ -95,7 +94,6 @@ def generate_error_message(exception: Union[APIError, APIErrors, Exception]) -> def handle_exception(exception: Union[APIError, APIErrors, Exception]) -> None: - log = logging.getLogger("langfuse") - log.debug(exception) + logger.debug(exception) error_message = generate_error_message(exception) - log.error(error_message) + logger.error(error_message) diff --git a/langfuse/_utils/prompt_cache.py b/langfuse/_utils/prompt_cache.py index 132dcb410..7d9c2298b 100644 --- a/langfuse/_utils/prompt_cache.py +++ b/langfuse/_utils/prompt_cache.py @@ -1,17 +1,24 @@ """@private""" import atexit -import logging +import os from datetime import datetime -from queue import Empty, Queue -from threading import Thread +from queue import Queue +from threading import RLock, Thread from typing import Callable, Dict, List, Optional, Set +from langfuse._client.environment_variables import ( + LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS, +) +from langfuse.logger import langfuse_logger as logger from langfuse.model import PromptClient -DEFAULT_PROMPT_CACHE_TTL_SECONDS = 60 +DEFAULT_PROMPT_CACHE_TTL_SECONDS = int( + os.getenv(LANGFUSE_PROMPT_CACHE_DEFAULT_TTL_SECONDS, 60) +) DEFAULT_PROMPT_CACHE_REFRESH_WORKERS = 1 +_SHUTDOWN_SENTINEL = object() class PromptCacheItem: @@ -28,7 +35,6 @@ def get_epoch_seconds() -> int: class PromptCacheRefreshConsumer(Thread): - _log = logging.getLogger("langfuse") _queue: Queue _identifier: int running: bool = True @@ -41,22 +47,24 @@ def __init__(self, queue: Queue, identifier: int): def run(self) -> None: while self.running: + task = self._queue.get() + + if task is _SHUTDOWN_SENTINEL: + self._queue.task_done() + break + + logger.debug( + f"PromptCacheRefreshConsumer processing task, {self._identifier}" + ) try: - task = self._queue.get(timeout=1) - self._log.debug( - f"PromptCacheRefreshConsumer processing task, {self._identifier}" + task() + # Task failed, but we still consider it processed + except Exception as e: + logger.warning( + f"PromptCacheRefreshConsumer encountered an error, cache was not refreshed: {self._identifier}, {e}" ) - try: - task() - # Task failed, but we still consider it processed - except Exception as e: - self._log.warning( - f"PromptCacheRefreshConsumer encountered an error, cache was not refreshed: {self._identifier}, {e}" - ) - self._queue.task_done() - except Empty: - pass + self._queue.task_done() def pause(self) -> None: """Pause the consumer.""" @@ -64,17 +72,18 @@ def pause(self) -> None: class PromptCacheTaskManager(object): - _log = logging.getLogger("langfuse") _consumers: List[PromptCacheRefreshConsumer] _threads: int _queue: Queue _processing_keys: Set[str] + _lock: RLock def __init__(self, threads: int = 1): self._queue = Queue() self._consumers = [] self._threads = threads self._processing_keys = set() + self._lock = RLock() for i in range(self._threads): consumer = PromptCacheRefreshConsumer(self._queue, i) @@ -84,32 +93,38 @@ def __init__(self, threads: int = 1): atexit.register(self.shutdown) def add_task(self, key: str, task: Callable[[], None]) -> None: - if key not in self._processing_keys: - self._log.debug(f"Adding prompt cache refresh task for key: {key}") - self._processing_keys.add(key) - wrapped_task = self._wrap_task(key, task) - self._queue.put((wrapped_task)) - else: - self._log.debug( - f"Prompt cache refresh task already submitted for key: {key}" - ) + with self._lock: + if key not in self._processing_keys: + logger.debug(f"Adding prompt cache refresh task for key: {key}") + self._processing_keys.add(key) + wrapped_task = self._wrap_task(key, task) + self._queue.put((wrapped_task)) + else: + logger.debug( + f"Prompt cache refresh task already submitted for key: {key}" + ) def active_tasks(self) -> int: - return len(self._processing_keys) + with self._lock: + return len(self._processing_keys) + + def wait_for_idle(self) -> None: + self._queue.join() def _wrap_task(self, key: str, task: Callable[[], None]) -> Callable[[], None]: def wrapped() -> None: - self._log.debug(f"Refreshing prompt cache for key: {key}") + logger.debug(f"Refreshing prompt cache for key: {key}") try: task() finally: - self._processing_keys.remove(key) - self._log.debug(f"Refreshed prompt cache for key: {key}") + with self._lock: + self._processing_keys.remove(key) + logger.debug(f"Refreshed prompt cache for key: {key}") return wrapped def shutdown(self) -> None: - self._log.debug( + logger.debug( f"Shutting down prompt refresh task manager, {len(self._consumers)} consumers,..." ) @@ -118,6 +133,9 @@ def shutdown(self) -> None: for consumer in self._consumers: consumer.pause() + for _ in self._consumers: + self._queue.put(_SHUTDOWN_SENTINEL) + for consumer in self._consumers: try: consumer.join() @@ -125,43 +143,75 @@ def shutdown(self) -> None: # consumer thread has not started pass - self._log.debug("Shutdown of prompt refresh task manager completed.") + logger.debug("Shutdown of prompt refresh task manager completed.") class PromptCache: _cache: Dict[str, PromptCacheItem] + _lock: RLock _task_manager: PromptCacheTaskManager """Task manager for refreshing cache""" - _log = logging.getLogger("langfuse") - def __init__( self, max_prompt_refresh_workers: int = DEFAULT_PROMPT_CACHE_REFRESH_WORKERS ): self._cache = {} + self._lock = RLock() self._task_manager = PromptCacheTaskManager(threads=max_prompt_refresh_workers) - self._log.debug("Prompt cache initialized.") + logger.debug("Prompt cache initialized.") def get(self, key: str) -> Optional[PromptCacheItem]: - return self._cache.get(key, None) + with self._lock: + return self._cache.get(key, None) def set(self, key: str, value: PromptClient, ttl_seconds: Optional[int]) -> None: if ttl_seconds is None: ttl_seconds = DEFAULT_PROMPT_CACHE_TTL_SECONDS - self._cache[key] = PromptCacheItem(value, ttl_seconds) + with self._lock: + self._cache[key] = PromptCacheItem(value, ttl_seconds) + + def delete(self, key: str) -> None: + with self._lock: + self._cache.pop(key, None) def invalidate(self, prompt_name: str) -> None: """Invalidate all cached prompts with the given prompt name.""" - for key in list(self._cache): - if key.startswith(prompt_name): - del self._cache[key] + with self._lock: + for key in list(self._cache): + if key.startswith(prompt_name): + del self._cache[key] def add_refresh_prompt_task(self, key: str, fetch_func: Callable[[], None]) -> None: - self._log.debug(f"Submitting refresh task for key: {key}") + logger.debug(f"Submitting refresh task for key: {key}") self._task_manager.add_task(key, fetch_func) + def add_refresh_prompt_task_if_current( + self, + key: str, + expected_item: PromptCacheItem, + fetch_func: Callable[[], None], + ) -> None: + with self._lock: + current_item = self._cache.get(key) + if ( + current_item is not None + and current_item is not expected_item + and not current_item.is_expired() + ): + logger.debug( + f"Skipping refresh task for key: {key} because cache is already fresh." + ) + return + + self.add_refresh_prompt_task(key, fetch_func) + + def clear(self) -> None: + """Clear the entire prompt cache, removing all cached prompts.""" + with self._lock: + self._cache.clear() + @staticmethod def generate_cache_key( name: str, *, version: Optional[int], label: Optional[str] diff --git a/langfuse/_utils/request.py b/langfuse/_utils/request.py index b106cee2f..402d0b5a7 100644 --- a/langfuse/_utils/request.py +++ b/langfuse/_utils/request.py @@ -1,13 +1,13 @@ """@private""" import json -import logging from base64 import b64encode from typing import Any, List, Union import httpx from langfuse._utils.serializer import EventSerializer +from langfuse.logger import langfuse_logger as logger class LangfuseClient: @@ -41,34 +41,31 @@ def generate_headers(self) -> dict: f"{self._public_key}:{self._secret_key}".encode("utf-8") ).decode("ascii"), "Content-Type": "application/json", - "x_langfuse_sdk_name": "python", - "x_langfuse_sdk_version": self._version, - "x_langfuse_public_key": self._public_key, + "x-langfuse-sdk-name": "python", + "x-langfuse-sdk-version": self._version, + "x-langfuse-public-key": self._public_key, } def batch_post(self, **kwargs: Any) -> httpx.Response: """Post the `kwargs` to the batch API endpoint for events""" - log = logging.getLogger("langfuse") - log.debug("uploading data: %s", kwargs) - res = self.post(**kwargs) + return self._process_response( res, success_message="data uploaded successfully", return_json=False ) def post(self, **kwargs: Any) -> httpx.Response: """Post the `kwargs` to the API""" - log = logging.getLogger("langfuse") url = self._remove_trailing_slash(self._base_url) + "/api/public/ingestion" data = json.dumps(kwargs, cls=EventSerializer) - log.debug("making request: %s to %s", data, url) + logger.debug("making request: %s to %s", data, url) headers = self.generate_headers() res = self._session.post( url, content=data, headers=headers, timeout=self._timeout ) if res.status_code == 200: - log.debug("data uploaded successfully") + logger.debug("data uploaded successfully") return res @@ -81,10 +78,9 @@ def _remove_trailing_slash(self, url: str) -> str: def _process_response( self, res: httpx.Response, success_message: str, *, return_json: bool = True ) -> Union[httpx.Response, Any]: - log = logging.getLogger("langfuse") - log.debug("received response: %s", res.text) + logger.debug("received response: %s", res.text) if res.status_code in (200, 201): - log.debug(success_message) + logger.debug(success_message) if return_json: try: return res.json() diff --git a/langfuse/_utils/serializer.py b/langfuse/_utils/serializer.py index 9232cc908..27294bf80 100644 --- a/langfuse/_utils/serializer.py +++ b/langfuse/_utils/serializer.py @@ -1,5 +1,6 @@ """@private""" +import datetime as dt import enum import math from asyncio import Queue @@ -14,12 +15,11 @@ from pydantic import BaseModel -from langfuse.api.core import pydantic_utilities, serialize_datetime from langfuse.media import LangfuseMedia # Attempt to import Serializable try: - from langchain.load.serializable import Serializable + from langchain_core.load.serializable import Serializable except ImportError: # If Serializable is not available, set it to a placeholder type class Serializable: # type: ignore @@ -28,7 +28,7 @@ class Serializable: # type: ignore # Attempt to import numpy try: - import numpy as np + import numpy as np # type: ignore[import-not-found] except ImportError: np = None # type: ignore @@ -36,11 +36,21 @@ class Serializable: # type: ignore class EventSerializer(JSONEncoder): + _MAX_DEPTH = 20 + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.seen: set[int] = set() # Track seen objects to detect circular references + self._depth = 0 def default(self, obj: Any) -> Any: + self._depth += 1 + try: + return self._default_inner(obj) + finally: + self._depth -= 1 + + def _default_inner(self, obj: Any) -> Any: try: if isinstance(obj, (datetime)): # Timezone-awareness check @@ -66,7 +76,7 @@ def default(self, obj: Any) -> Any: return "NaN" if isinstance(obj, float) and math.isinf(obj): - return "Infinity" + return "-Infinity" if obj < 0 else "Infinity" if isinstance(obj, (Exception, KeyboardInterrupt)): return f"{type(obj).__name__}: {str(obj)}" @@ -82,9 +92,6 @@ def default(self, obj: Any) -> Any: if isinstance(obj, Queue): return type(obj).__name__ - if is_dataclass(obj): - return asdict(obj) # type: ignore - if isinstance(obj, UUID): return str(obj) @@ -97,26 +104,9 @@ def default(self, obj: Any) -> Any: if isinstance(obj, (date)): return obj.isoformat() - if isinstance(obj, BaseModel): - obj.model_rebuild() if pydantic_utilities.IS_PYDANTIC_V2 else obj.update_forward_refs() # This method forces the OpenAI model to instantiate its serializer to avoid errors when serializing - - # For LlamaIndex models, we need to rebuild the raw model as well if they include OpenAI models - if isinstance(raw := getattr(obj, "raw", None), BaseModel): - raw.model_rebuild() if pydantic_utilities.IS_PYDANTIC_V2 else raw.update_forward_refs() - - return ( - obj.model_dump() - if pydantic_utilities.IS_PYDANTIC_V2 - else obj.dict() - ) - if isinstance(obj, Path): return str(obj) - # if langchain is not available, the Serializable type is NoneType - if Serializable is not type(None) and isinstance(obj, Serializable): # type: ignore - return obj.to_json() - # 64-bit integers might overflow the JavaScript safe integer range. # Since Node.js is run on the server that handles the serialized value, # we need to ensure that integers outside the safe range are converted to strings. @@ -127,6 +117,25 @@ def default(self, obj: Any) -> Any: if isinstance(obj, (str, float, type(None))): return obj + if self._depth >= self._MAX_DEPTH: + return f"<{type(obj).__name__}>" + + if is_dataclass(obj): + return asdict(obj) # type: ignore + + if isinstance(obj, BaseModel): + obj.model_rebuild() + + # For LlamaIndex models, we need to rebuild the raw model as well if they include OpenAI models + if isinstance(raw := getattr(obj, "raw", None), BaseModel): + raw.model_rebuild() + + return obj.model_dump() + + # if langchain is not available, the Serializable type is NoneType + if Serializable is not type(None) and isinstance(obj, Serializable): # type: ignore + return obj.to_json() + if isinstance(obj, (tuple, set, frozenset)): return list(obj) @@ -142,9 +151,10 @@ def default(self, obj: Any) -> Any: return [self.default(item) for item in obj] if hasattr(obj, "__slots__"): - return self.default( - {slot: getattr(obj, slot, None) for slot in obj.__slots__} - ) + return { + slot: self.default(getattr(obj, slot, None)) + for slot in obj.__slots__ + } elif hasattr(obj, "__dict__"): obj_id = id(obj) @@ -171,6 +181,7 @@ def default(self, obj: Any) -> Any: def encode(self, obj: Any) -> str: self.seen.clear() # Clear seen objects before each encode call + self._depth = 0 try: return super().encode(self.default(obj)) @@ -188,3 +199,22 @@ def is_js_safe_integer(value: int) -> bool: min_safe_int = -(2**53) + 1 return min_safe_int <= value <= max_safe_int + + +def serialize_datetime(v: dt.datetime) -> str: + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname( + None + ): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/langfuse/_version.py b/langfuse/_version.py new file mode 100644 index 000000000..38e2d8325 --- /dev/null +++ b/langfuse/_version.py @@ -0,0 +1,13 @@ +from functools import lru_cache +from importlib.metadata import PackageNotFoundError, version + + +@lru_cache(maxsize=1) +def get_langfuse_version() -> str: + try: + return version("langfuse") + except PackageNotFoundError: + return "0.0.0" + + +__version__ = get_langfuse_version() diff --git a/langfuse/api/.fern/metadata.json b/langfuse/api/.fern/metadata.json new file mode 100644 index 000000000..4f1a155c9 --- /dev/null +++ b/langfuse/api/.fern/metadata.json @@ -0,0 +1,14 @@ +{ + "cliVersion": "3.30.3", + "generatorName": "fernapi/fern-python-sdk", + "generatorVersion": "4.46.2", + "generatorConfig": { + "pydantic_config": { + "enum_type": "python_enums", + "version": "v2" + }, + "client": { + "class_name": "LangfuseAPI" + } + } +} \ No newline at end of file diff --git a/langfuse/api/README.md b/langfuse/api/README.md deleted file mode 100644 index feb6512ef..000000000 --- a/langfuse/api/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# Langfuse Python Library - -[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Langfuse%2FPython) -[![pypi](https://img.shields.io/pypi/v/langfuse)](https://pypi.python.org/pypi/langfuse) - -The Langfuse Python library provides convenient access to the Langfuse API from Python. - -## Installation - -```sh -pip install langfuse -``` - -## Usage - -Instantiate and use the client with the following: - -```python -from langfuse import AnnotationQueueObjectType, CreateAnnotationQueueItemRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.annotation_queues.create_queue_item( - queue_id="queueId", - request=CreateAnnotationQueueItemRequest( - object_id="objectId", - object_type=AnnotationQueueObjectType.TRACE, - ), -) -``` - -## Async Client - -The SDK also exports an `async` client so that you can make non-blocking calls to our API. - -```python -import asyncio - -from langfuse import AnnotationQueueObjectType, CreateAnnotationQueueItemRequest -from langfuse.client import AsyncFernLangfuse - -client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) - - -async def main() -> None: - await client.annotation_queues.create_queue_item( - queue_id="queueId", - request=CreateAnnotationQueueItemRequest( - object_id="objectId", - object_type=AnnotationQueueObjectType.TRACE, - ), - ) - - -asyncio.run(main()) -``` - -## Exception Handling - -When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error -will be thrown. - -```python -from .api_error import ApiError - -try: - client.annotation_queues.create_queue_item(...) -except ApiError as e: - print(e.status_code) - print(e.body) -``` - -## Advanced - -### Retries - -The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long -as the request is deemed retriable and the number of retry attempts has not grown larger than the configured -retry limit (default: 2). - -A request is deemed retriable when any of the following HTTP status codes is returned: - -- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) -- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) -- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) - -Use the `max_retries` request option to configure this behavior. - -```python -client.annotation_queues.create_queue_item(...,{ - max_retries=1 -}) -``` - -### Timeouts - -The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. - -```python - -from langfuse.client import FernLangfuse - -client = FernLangfuse(..., { timeout=20.0 }, ) - - -# Override timeout for a specific method -client.annotation_queues.create_queue_item(...,{ - timeout_in_seconds=1 -}) -``` - -### Custom Client - -You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies -and transports. -```python -import httpx -from langfuse.client import FernLangfuse - -client = FernLangfuse( - ..., - http_client=httpx.Client( - proxies="http://my.test.proxy.example.com", - transport=httpx.HTTPTransport(local_address="0.0.0.0"), - ), -) -``` - -## Contributing - -While we value open-source contributions to this SDK, this library is generated programmatically. -Additions made directly to this library would have to be moved over to our generation code, -otherwise they would be overwritten upon the next generated release. Feel free to open a PR as -a proof of concept, but know that we will not be able to merge it as-is. We suggest opening -an issue first to discuss with us! - -On the other hand, contributions to the README are always very welcome! diff --git a/langfuse/api/__init__.py b/langfuse/api/__init__.py index 2a274a811..46985c0b9 100644 --- a/langfuse/api/__init__.py +++ b/langfuse/api/__init__.py @@ -1,221 +1,655 @@ # This file was auto-generated by Fern from our API Definition. -from .resources import ( - AccessDeniedError, - AnnotationQueue, - AnnotationQueueItem, - AnnotationQueueObjectType, - AnnotationQueueStatus, - ApiKeyDeletionResponse, - ApiKeyList, - ApiKeyResponse, - ApiKeySummary, - AuthenticationScheme, - BaseEvent, - BasePrompt, - BaseScore, - BaseScoreV1, - BooleanScore, - BooleanScoreV1, - BulkConfig, - CategoricalScore, - CategoricalScoreV1, - ChatMessage, - ChatMessageWithPlaceholders, - ChatMessageWithPlaceholders_Chatmessage, - ChatMessageWithPlaceholders_Placeholder, - ChatPrompt, - Comment, - CommentObjectType, - ConfigCategory, - CreateAnnotationQueueItemRequest, - CreateChatPromptRequest, - CreateCommentRequest, - CreateCommentResponse, - CreateDatasetItemRequest, - CreateDatasetRequest, - CreateDatasetRunItemRequest, - CreateEventBody, - CreateEventEvent, - CreateGenerationBody, - CreateGenerationEvent, - CreateModelRequest, - CreateObservationEvent, - CreatePromptRequest, - CreatePromptRequest_Chat, - CreatePromptRequest_Text, - CreateScoreConfigRequest, - CreateScoreRequest, - CreateScoreResponse, - CreateScoreValue, - CreateSpanBody, - CreateSpanEvent, - CreateTextPromptRequest, - Dataset, - DatasetItem, - DatasetRun, - DatasetRunItem, - DatasetRunWithItems, - DatasetStatus, - DeleteAnnotationQueueItemResponse, - DeleteDatasetItemResponse, - DeleteDatasetRunResponse, - DeleteTraceResponse, - EmptyResponse, - Error, - FilterConfig, - GetCommentsResponse, - GetMediaResponse, - GetMediaUploadUrlRequest, - GetMediaUploadUrlResponse, - GetScoresResponse, - GetScoresResponseData, - GetScoresResponseDataBoolean, - GetScoresResponseDataCategorical, - GetScoresResponseDataNumeric, - GetScoresResponseData_Boolean, - GetScoresResponseData_Categorical, - GetScoresResponseData_Numeric, - GetScoresResponseTraceData, - HealthResponse, - IngestionError, - IngestionEvent, - IngestionEvent_EventCreate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, - IngestionEvent_ScoreCreate, - IngestionEvent_SdkLog, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_TraceCreate, - IngestionResponse, - IngestionSuccess, - IngestionUsage, - MapValue, - MediaContentType, - MembershipRequest, - MembershipResponse, - MembershipRole, - MembershipsResponse, - MethodNotAllowedError, - MetricsResponse, - Model, - ModelPrice, - ModelUsageUnit, - NotFoundError, - NumericScore, - NumericScoreV1, - Observation, - ObservationBody, - ObservationLevel, - ObservationType, - Observations, - ObservationsView, - ObservationsViews, - OpenAiCompletionUsageSchema, - OpenAiResponseUsageSchema, - OpenAiUsage, - OptionalObservationBody, - OrganizationProject, - OrganizationProjectsResponse, - PaginatedAnnotationQueueItems, - PaginatedAnnotationQueues, - PaginatedDatasetItems, - PaginatedDatasetRunItems, - PaginatedDatasetRuns, - PaginatedDatasets, - PaginatedModels, - PaginatedSessions, - PatchMediaBody, - PlaceholderMessage, - Project, - ProjectDeletionResponse, - Projects, - Prompt, - PromptMeta, - PromptMetaListResponse, - Prompt_Chat, - Prompt_Text, - ResourceMeta, - ResourceType, - ResourceTypesResponse, - SchemaExtension, - SchemaResource, - SchemasResponse, - ScimEmail, - ScimFeatureSupport, - ScimName, - ScimUser, - ScimUsersListResponse, - Score, - ScoreBody, - ScoreConfig, - ScoreConfigs, - ScoreDataType, - ScoreEvent, - ScoreSource, - ScoreV1, - ScoreV1_Boolean, - ScoreV1_Categorical, - ScoreV1_Numeric, - Score_Boolean, - Score_Categorical, - Score_Numeric, - SdkLogBody, - SdkLogEvent, - ServiceProviderConfig, - ServiceUnavailableError, - Session, - SessionWithTraces, - Sort, - TextPrompt, - Trace, - TraceBody, - TraceEvent, - TraceWithDetails, - TraceWithFullDetails, - Traces, - UnauthorizedError, - UpdateAnnotationQueueItemRequest, - UpdateEventBody, - UpdateGenerationBody, - UpdateGenerationEvent, - UpdateObservationEvent, - UpdateSpanBody, - UpdateSpanEvent, - Usage, - UsageDetails, - UserMeta, - annotation_queues, - comments, - commons, - dataset_items, - dataset_run_items, - datasets, - health, - ingestion, - media, - metrics, - models, - observations, - organizations, - projects, - prompt_version, - prompts, - scim, - score, - score_configs, - score_v_2, - sessions, - trace, - utils, -) +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from . import ( + annotation_queues, + blob_storage_integrations, + comments, + commons, + dataset_items, + dataset_run_items, + datasets, + health, + ingestion, + legacy, + llm_connections, + media, + metrics, + models, + observations, + opentelemetry, + organizations, + projects, + prompt_version, + prompts, + scim, + score_configs, + scores, + scores_v3, + sessions, + trace, + unstable, + utils, + ) + from .annotation_queues import ( + AnnotationQueue, + AnnotationQueueAssignmentRequest, + AnnotationQueueItem, + AnnotationQueueObjectType, + AnnotationQueueStatus, + CreateAnnotationQueueAssignmentResponse, + CreateAnnotationQueueItemRequest, + CreateAnnotationQueueRequest, + DeleteAnnotationQueueAssignmentResponse, + DeleteAnnotationQueueItemResponse, + PaginatedAnnotationQueueItems, + PaginatedAnnotationQueues, + UpdateAnnotationQueueItemRequest, + ) + from .blob_storage_integrations import ( + BlobStorageExportFieldGroup, + BlobStorageExportFrequency, + BlobStorageExportMode, + BlobStorageExportSource, + BlobStorageIntegrationDeletionResponse, + BlobStorageIntegrationFileType, + BlobStorageIntegrationResponse, + BlobStorageIntegrationStatusResponse, + BlobStorageIntegrationType, + BlobStorageIntegrationsResponse, + BlobStorageSyncStatus, + CreateBlobStorageIntegrationRequest, + ) + from .client import AsyncLangfuseAPI, LangfuseAPI + from .comments import ( + CreateCommentRequest, + CreateCommentResponse, + GetCommentsResponse, + ) + from .commons import ( + AccessDeniedError, + BaseScore, + BaseScoreV1, + BooleanScore, + BooleanScoreV1, + CategoricalScore, + CategoricalScoreV1, + Comment, + CommentObjectType, + ConfigCategory, + CorrectionScore, + CreateScoreValue, + Dataset, + DatasetItem, + DatasetRun, + DatasetRunItem, + DatasetRunWithItems, + DatasetStatus, + Error, + MapValue, + MethodNotAllowedError, + Model, + ModelPrice, + ModelUsageUnit, + NotFoundError, + NumericScore, + NumericScoreV1, + Observation, + ObservationLevel, + ObservationV2, + ObservationsView, + PricingTier, + PricingTierCondition, + PricingTierInput, + PricingTierOperator, + Score, + ScoreConfig, + ScoreConfigDataType, + ScoreDataType, + ScoreSource, + ScoreV1, + ScoreV1_Boolean, + ScoreV1_Categorical, + ScoreV1_Numeric, + ScoreV1_Text, + Score_Boolean, + Score_Categorical, + Score_Correction, + Score_Numeric, + Score_Text, + Session, + SessionWithTraces, + TextScore, + TextScoreV1, + Trace, + TraceWithDetails, + TraceWithFullDetails, + UnauthorizedError, + Usage, + ) + from .dataset_items import ( + CreateDatasetItemRequest, + DeleteDatasetItemResponse, + PaginatedDatasetItems, + ) + from .dataset_run_items import CreateDatasetRunItemRequest, PaginatedDatasetRunItems + from .datasets import ( + CreateDatasetRequest, + DeleteDatasetRunResponse, + PaginatedDatasetRuns, + PaginatedDatasets, + ) + from .health import HealthResponse, ServiceUnavailableError + from .ingestion import ( + BaseEvent, + CreateEventBody, + CreateEventEvent, + CreateGenerationBody, + CreateGenerationEvent, + CreateObservationEvent, + CreateSpanBody, + CreateSpanEvent, + IngestionError, + IngestionEvent, + IngestionEvent_EventCreate, + IngestionEvent_GenerationCreate, + IngestionEvent_GenerationUpdate, + IngestionEvent_ObservationCreate, + IngestionEvent_ObservationUpdate, + IngestionEvent_ScoreCreate, + IngestionEvent_SdkLog, + IngestionEvent_SpanCreate, + IngestionEvent_SpanUpdate, + IngestionEvent_TraceCreate, + IngestionResponse, + IngestionSuccess, + IngestionUsage, + ObservationBody, + ObservationType, + OpenAiCompletionUsageSchema, + OpenAiResponseUsageSchema, + OpenAiUsage, + OptionalObservationBody, + ScoreBody, + ScoreEvent, + SdkLogBody, + SdkLogEvent, + TraceBody, + TraceEvent, + UpdateEventBody, + UpdateGenerationBody, + UpdateGenerationEvent, + UpdateObservationEvent, + UpdateSpanBody, + UpdateSpanEvent, + UsageDetails, + ) + from .llm_connections import ( + DeleteLlmConnectionResponse, + LlmAdapter, + LlmConnection, + PaginatedLlmConnections, + UpsertLlmConnectionRequest, + ) + from .media import ( + GetMediaResponse, + GetMediaUploadUrlRequest, + GetMediaUploadUrlResponse, + MediaContentType, + PatchMediaBody, + ) + from .metrics import MetricsV2Response + from .models import CreateModelRequest, PaginatedModels + from .observations import ObservationsV2Meta, ObservationsV2Response + from .opentelemetry import ( + OtelAttribute, + OtelAttributeValue, + OtelResource, + OtelResourceSpan, + OtelScope, + OtelScopeSpan, + OtelSpan, + OtelTraceResponse, + ) + from .organizations import ( + DeleteMembershipRequest, + MembershipDeletionResponse, + MembershipRequest, + MembershipResponse, + MembershipRole, + MembershipsResponse, + OrganizationApiKey, + OrganizationApiKeysResponse, + OrganizationProject, + OrganizationProjectsResponse, + ) + from .projects import ( + ApiKeyDeletionResponse, + ApiKeyList, + ApiKeyResponse, + ApiKeySummary, + Organization, + Project, + ProjectDeletionResponse, + Projects, + ) + from .prompts import ( + BasePrompt, + ChatMessage, + ChatMessageType, + ChatMessageWithPlaceholders, + ChatPrompt, + CreateChatPromptRequest, + CreateChatPromptType, + CreatePromptRequest, + CreateTextPromptRequest, + CreateTextPromptType, + PlaceholderMessage, + PlaceholderMessageType, + Prompt, + PromptMeta, + PromptMetaListResponse, + PromptType, + Prompt_Chat, + Prompt_Text, + TextPrompt, + ) + from .scim import ( + AuthenticationScheme, + BulkConfig, + EmptyResponse, + FilterConfig, + ResourceMeta, + ResourceType, + ResourceTypesResponse, + SchemaExtension, + SchemaResource, + SchemasResponse, + ScimEmail, + ScimFeatureSupport, + ScimName, + ScimUser, + ScimUsersListResponse, + ServiceProviderConfig, + UserMeta, + ) + from .score_configs import ( + CreateScoreConfigRequest, + ScoreConfigs, + UpdateScoreConfigRequest, + ) + from .scores import ( + GetScoresResponse, + GetScoresResponseData, + GetScoresResponseDataBoolean, + GetScoresResponseDataCategorical, + GetScoresResponseDataCorrection, + GetScoresResponseDataNumeric, + GetScoresResponseDataText, + GetScoresResponseData_Boolean, + GetScoresResponseData_Categorical, + GetScoresResponseData_Correction, + GetScoresResponseData_Numeric, + GetScoresResponseData_Text, + GetScoresResponseTraceData, + ) + from .scores_v3 import ( + BaseScoreV3, + BooleanScoreV3, + CategoricalScoreV3, + CorrectionScoreV3, + GetScoresV3Meta, + GetScoresV3Response, + NumericScoreV3, + ScoreSubjectExperimentV3, + ScoreSubjectObservationV3, + ScoreSubjectSessionV3, + ScoreSubjectTraceV3, + ScoreSubjectV3, + ScoreSubjectV3_Experiment, + ScoreSubjectV3_Observation, + ScoreSubjectV3_Session, + ScoreSubjectV3_Trace, + ScoreV3, + ScoreV3_Boolean, + ScoreV3_Categorical, + ScoreV3_Correction, + ScoreV3_Numeric, + ScoreV3_Text, + TextScoreV3, + ) + from .sessions import PaginatedSessions + from .trace import DeleteTraceResponse, Sort, Traces +_dynamic_imports: typing.Dict[str, str] = { + "AccessDeniedError": ".commons", + "AnnotationQueue": ".annotation_queues", + "AnnotationQueueAssignmentRequest": ".annotation_queues", + "AnnotationQueueItem": ".annotation_queues", + "AnnotationQueueObjectType": ".annotation_queues", + "AnnotationQueueStatus": ".annotation_queues", + "ApiKeyDeletionResponse": ".projects", + "ApiKeyList": ".projects", + "ApiKeyResponse": ".projects", + "ApiKeySummary": ".projects", + "AsyncLangfuseAPI": ".client", + "AuthenticationScheme": ".scim", + "BaseEvent": ".ingestion", + "BasePrompt": ".prompts", + "BaseScore": ".commons", + "BaseScoreV1": ".commons", + "BaseScoreV3": ".scores_v3", + "BlobStorageExportFieldGroup": ".blob_storage_integrations", + "BlobStorageExportFrequency": ".blob_storage_integrations", + "BlobStorageExportMode": ".blob_storage_integrations", + "BlobStorageExportSource": ".blob_storage_integrations", + "BlobStorageIntegrationDeletionResponse": ".blob_storage_integrations", + "BlobStorageIntegrationFileType": ".blob_storage_integrations", + "BlobStorageIntegrationResponse": ".blob_storage_integrations", + "BlobStorageIntegrationStatusResponse": ".blob_storage_integrations", + "BlobStorageIntegrationType": ".blob_storage_integrations", + "BlobStorageIntegrationsResponse": ".blob_storage_integrations", + "BlobStorageSyncStatus": ".blob_storage_integrations", + "BooleanScore": ".commons", + "BooleanScoreV1": ".commons", + "BooleanScoreV3": ".scores_v3", + "BulkConfig": ".scim", + "CategoricalScore": ".commons", + "CategoricalScoreV1": ".commons", + "CategoricalScoreV3": ".scores_v3", + "ChatMessage": ".prompts", + "ChatMessageType": ".prompts", + "ChatMessageWithPlaceholders": ".prompts", + "ChatPrompt": ".prompts", + "Comment": ".commons", + "CommentObjectType": ".commons", + "ConfigCategory": ".commons", + "CorrectionScore": ".commons", + "CorrectionScoreV3": ".scores_v3", + "CreateAnnotationQueueAssignmentResponse": ".annotation_queues", + "CreateAnnotationQueueItemRequest": ".annotation_queues", + "CreateAnnotationQueueRequest": ".annotation_queues", + "CreateBlobStorageIntegrationRequest": ".blob_storage_integrations", + "CreateChatPromptRequest": ".prompts", + "CreateChatPromptType": ".prompts", + "CreateCommentRequest": ".comments", + "CreateCommentResponse": ".comments", + "CreateDatasetItemRequest": ".dataset_items", + "CreateDatasetRequest": ".datasets", + "CreateDatasetRunItemRequest": ".dataset_run_items", + "CreateEventBody": ".ingestion", + "CreateEventEvent": ".ingestion", + "CreateGenerationBody": ".ingestion", + "CreateGenerationEvent": ".ingestion", + "CreateModelRequest": ".models", + "CreateObservationEvent": ".ingestion", + "CreatePromptRequest": ".prompts", + "CreateScoreConfigRequest": ".score_configs", + "CreateScoreValue": ".commons", + "CreateSpanBody": ".ingestion", + "CreateSpanEvent": ".ingestion", + "CreateTextPromptRequest": ".prompts", + "CreateTextPromptType": ".prompts", + "Dataset": ".commons", + "DatasetItem": ".commons", + "DatasetRun": ".commons", + "DatasetRunItem": ".commons", + "DatasetRunWithItems": ".commons", + "DatasetStatus": ".commons", + "DeleteAnnotationQueueAssignmentResponse": ".annotation_queues", + "DeleteAnnotationQueueItemResponse": ".annotation_queues", + "DeleteDatasetItemResponse": ".dataset_items", + "DeleteDatasetRunResponse": ".datasets", + "DeleteLlmConnectionResponse": ".llm_connections", + "DeleteMembershipRequest": ".organizations", + "DeleteTraceResponse": ".trace", + "EmptyResponse": ".scim", + "Error": ".commons", + "FilterConfig": ".scim", + "GetCommentsResponse": ".comments", + "GetMediaResponse": ".media", + "GetMediaUploadUrlRequest": ".media", + "GetMediaUploadUrlResponse": ".media", + "GetScoresResponse": ".scores", + "GetScoresResponseData": ".scores", + "GetScoresResponseDataBoolean": ".scores", + "GetScoresResponseDataCategorical": ".scores", + "GetScoresResponseDataCorrection": ".scores", + "GetScoresResponseDataNumeric": ".scores", + "GetScoresResponseDataText": ".scores", + "GetScoresResponseData_Boolean": ".scores", + "GetScoresResponseData_Categorical": ".scores", + "GetScoresResponseData_Correction": ".scores", + "GetScoresResponseData_Numeric": ".scores", + "GetScoresResponseData_Text": ".scores", + "GetScoresResponseTraceData": ".scores", + "GetScoresV3Meta": ".scores_v3", + "GetScoresV3Response": ".scores_v3", + "HealthResponse": ".health", + "IngestionError": ".ingestion", + "IngestionEvent": ".ingestion", + "IngestionEvent_EventCreate": ".ingestion", + "IngestionEvent_GenerationCreate": ".ingestion", + "IngestionEvent_GenerationUpdate": ".ingestion", + "IngestionEvent_ObservationCreate": ".ingestion", + "IngestionEvent_ObservationUpdate": ".ingestion", + "IngestionEvent_ScoreCreate": ".ingestion", + "IngestionEvent_SdkLog": ".ingestion", + "IngestionEvent_SpanCreate": ".ingestion", + "IngestionEvent_SpanUpdate": ".ingestion", + "IngestionEvent_TraceCreate": ".ingestion", + "IngestionResponse": ".ingestion", + "IngestionSuccess": ".ingestion", + "IngestionUsage": ".ingestion", + "LangfuseAPI": ".client", + "LlmAdapter": ".llm_connections", + "LlmConnection": ".llm_connections", + "MapValue": ".commons", + "MediaContentType": ".media", + "MembershipDeletionResponse": ".organizations", + "MembershipRequest": ".organizations", + "MembershipResponse": ".organizations", + "MembershipRole": ".organizations", + "MembershipsResponse": ".organizations", + "MethodNotAllowedError": ".commons", + "MetricsV2Response": ".metrics", + "Model": ".commons", + "ModelPrice": ".commons", + "ModelUsageUnit": ".commons", + "NotFoundError": ".commons", + "NumericScore": ".commons", + "NumericScoreV1": ".commons", + "NumericScoreV3": ".scores_v3", + "Observation": ".commons", + "ObservationBody": ".ingestion", + "ObservationLevel": ".commons", + "ObservationType": ".ingestion", + "ObservationV2": ".commons", + "ObservationsV2Meta": ".observations", + "ObservationsV2Response": ".observations", + "ObservationsView": ".commons", + "OpenAiCompletionUsageSchema": ".ingestion", + "OpenAiResponseUsageSchema": ".ingestion", + "OpenAiUsage": ".ingestion", + "OptionalObservationBody": ".ingestion", + "Organization": ".projects", + "OrganizationApiKey": ".organizations", + "OrganizationApiKeysResponse": ".organizations", + "OrganizationProject": ".organizations", + "OrganizationProjectsResponse": ".organizations", + "OtelAttribute": ".opentelemetry", + "OtelAttributeValue": ".opentelemetry", + "OtelResource": ".opentelemetry", + "OtelResourceSpan": ".opentelemetry", + "OtelScope": ".opentelemetry", + "OtelScopeSpan": ".opentelemetry", + "OtelSpan": ".opentelemetry", + "OtelTraceResponse": ".opentelemetry", + "PaginatedAnnotationQueueItems": ".annotation_queues", + "PaginatedAnnotationQueues": ".annotation_queues", + "PaginatedDatasetItems": ".dataset_items", + "PaginatedDatasetRunItems": ".dataset_run_items", + "PaginatedDatasetRuns": ".datasets", + "PaginatedDatasets": ".datasets", + "PaginatedLlmConnections": ".llm_connections", + "PaginatedModels": ".models", + "PaginatedSessions": ".sessions", + "PatchMediaBody": ".media", + "PlaceholderMessage": ".prompts", + "PlaceholderMessageType": ".prompts", + "PricingTier": ".commons", + "PricingTierCondition": ".commons", + "PricingTierInput": ".commons", + "PricingTierOperator": ".commons", + "Project": ".projects", + "ProjectDeletionResponse": ".projects", + "Projects": ".projects", + "Prompt": ".prompts", + "PromptMeta": ".prompts", + "PromptMetaListResponse": ".prompts", + "PromptType": ".prompts", + "Prompt_Chat": ".prompts", + "Prompt_Text": ".prompts", + "ResourceMeta": ".scim", + "ResourceType": ".scim", + "ResourceTypesResponse": ".scim", + "SchemaExtension": ".scim", + "SchemaResource": ".scim", + "SchemasResponse": ".scim", + "ScimEmail": ".scim", + "ScimFeatureSupport": ".scim", + "ScimName": ".scim", + "ScimUser": ".scim", + "ScimUsersListResponse": ".scim", + "Score": ".commons", + "ScoreBody": ".ingestion", + "ScoreConfig": ".commons", + "ScoreConfigDataType": ".commons", + "ScoreConfigs": ".score_configs", + "ScoreDataType": ".commons", + "ScoreEvent": ".ingestion", + "ScoreSource": ".commons", + "ScoreSubjectExperimentV3": ".scores_v3", + "ScoreSubjectObservationV3": ".scores_v3", + "ScoreSubjectSessionV3": ".scores_v3", + "ScoreSubjectTraceV3": ".scores_v3", + "ScoreSubjectV3": ".scores_v3", + "ScoreSubjectV3_Experiment": ".scores_v3", + "ScoreSubjectV3_Observation": ".scores_v3", + "ScoreSubjectV3_Session": ".scores_v3", + "ScoreSubjectV3_Trace": ".scores_v3", + "ScoreV1": ".commons", + "ScoreV1_Boolean": ".commons", + "ScoreV1_Categorical": ".commons", + "ScoreV1_Numeric": ".commons", + "ScoreV1_Text": ".commons", + "ScoreV3": ".scores_v3", + "ScoreV3_Boolean": ".scores_v3", + "ScoreV3_Categorical": ".scores_v3", + "ScoreV3_Correction": ".scores_v3", + "ScoreV3_Numeric": ".scores_v3", + "ScoreV3_Text": ".scores_v3", + "Score_Boolean": ".commons", + "Score_Categorical": ".commons", + "Score_Correction": ".commons", + "Score_Numeric": ".commons", + "Score_Text": ".commons", + "SdkLogBody": ".ingestion", + "SdkLogEvent": ".ingestion", + "ServiceProviderConfig": ".scim", + "ServiceUnavailableError": ".health", + "Session": ".commons", + "SessionWithTraces": ".commons", + "Sort": ".trace", + "TextPrompt": ".prompts", + "TextScore": ".commons", + "TextScoreV1": ".commons", + "TextScoreV3": ".scores_v3", + "Trace": ".commons", + "TraceBody": ".ingestion", + "TraceEvent": ".ingestion", + "TraceWithDetails": ".commons", + "TraceWithFullDetails": ".commons", + "Traces": ".trace", + "UnauthorizedError": ".commons", + "UpdateAnnotationQueueItemRequest": ".annotation_queues", + "UpdateEventBody": ".ingestion", + "UpdateGenerationBody": ".ingestion", + "UpdateGenerationEvent": ".ingestion", + "UpdateObservationEvent": ".ingestion", + "UpdateScoreConfigRequest": ".score_configs", + "UpdateSpanBody": ".ingestion", + "UpdateSpanEvent": ".ingestion", + "UpsertLlmConnectionRequest": ".llm_connections", + "Usage": ".commons", + "UsageDetails": ".ingestion", + "UserMeta": ".scim", + "annotation_queues": ".annotation_queues", + "blob_storage_integrations": ".blob_storage_integrations", + "comments": ".comments", + "commons": ".commons", + "dataset_items": ".dataset_items", + "dataset_run_items": ".dataset_run_items", + "datasets": ".datasets", + "health": ".health", + "ingestion": ".ingestion", + "legacy": ".legacy", + "llm_connections": ".llm_connections", + "media": ".media", + "metrics": ".metrics", + "models": ".models", + "observations": ".observations", + "opentelemetry": ".opentelemetry", + "organizations": ".organizations", + "projects": ".projects", + "prompt_version": ".prompt_version", + "prompts": ".prompts", + "scim": ".scim", + "score_configs": ".score_configs", + "scores": ".scores", + "scores_v3": ".scores_v3", + "sessions": ".sessions", + "trace": ".trace", + "unstable": ".unstable", + "utils": ".utils", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + __all__ = [ "AccessDeniedError", "AnnotationQueue", + "AnnotationQueueAssignmentRequest", "AnnotationQueueItem", "AnnotationQueueObjectType", "AnnotationQueueStatus", @@ -223,26 +657,46 @@ "ApiKeyList", "ApiKeyResponse", "ApiKeySummary", + "AsyncLangfuseAPI", "AuthenticationScheme", "BaseEvent", "BasePrompt", "BaseScore", "BaseScoreV1", + "BaseScoreV3", + "BlobStorageExportFieldGroup", + "BlobStorageExportFrequency", + "BlobStorageExportMode", + "BlobStorageExportSource", + "BlobStorageIntegrationDeletionResponse", + "BlobStorageIntegrationFileType", + "BlobStorageIntegrationResponse", + "BlobStorageIntegrationStatusResponse", + "BlobStorageIntegrationType", + "BlobStorageIntegrationsResponse", + "BlobStorageSyncStatus", "BooleanScore", "BooleanScoreV1", + "BooleanScoreV3", "BulkConfig", "CategoricalScore", "CategoricalScoreV1", + "CategoricalScoreV3", "ChatMessage", + "ChatMessageType", "ChatMessageWithPlaceholders", - "ChatMessageWithPlaceholders_Chatmessage", - "ChatMessageWithPlaceholders_Placeholder", "ChatPrompt", "Comment", "CommentObjectType", "ConfigCategory", + "CorrectionScore", + "CorrectionScoreV3", + "CreateAnnotationQueueAssignmentResponse", "CreateAnnotationQueueItemRequest", + "CreateAnnotationQueueRequest", + "CreateBlobStorageIntegrationRequest", "CreateChatPromptRequest", + "CreateChatPromptType", "CreateCommentRequest", "CreateCommentResponse", "CreateDatasetItemRequest", @@ -255,24 +709,24 @@ "CreateModelRequest", "CreateObservationEvent", "CreatePromptRequest", - "CreatePromptRequest_Chat", - "CreatePromptRequest_Text", "CreateScoreConfigRequest", - "CreateScoreRequest", - "CreateScoreResponse", "CreateScoreValue", "CreateSpanBody", "CreateSpanEvent", "CreateTextPromptRequest", + "CreateTextPromptType", "Dataset", "DatasetItem", "DatasetRun", "DatasetRunItem", "DatasetRunWithItems", "DatasetStatus", + "DeleteAnnotationQueueAssignmentResponse", "DeleteAnnotationQueueItemResponse", "DeleteDatasetItemResponse", "DeleteDatasetRunResponse", + "DeleteLlmConnectionResponse", + "DeleteMembershipRequest", "DeleteTraceResponse", "EmptyResponse", "Error", @@ -285,11 +739,17 @@ "GetScoresResponseData", "GetScoresResponseDataBoolean", "GetScoresResponseDataCategorical", + "GetScoresResponseDataCorrection", "GetScoresResponseDataNumeric", + "GetScoresResponseDataText", "GetScoresResponseData_Boolean", "GetScoresResponseData_Categorical", + "GetScoresResponseData_Correction", "GetScoresResponseData_Numeric", + "GetScoresResponseData_Text", "GetScoresResponseTraceData", + "GetScoresV3Meta", + "GetScoresV3Response", "HealthResponse", "IngestionError", "IngestionEvent", @@ -306,49 +766,73 @@ "IngestionResponse", "IngestionSuccess", "IngestionUsage", + "LangfuseAPI", + "LlmAdapter", + "LlmConnection", "MapValue", "MediaContentType", + "MembershipDeletionResponse", "MembershipRequest", "MembershipResponse", "MembershipRole", "MembershipsResponse", "MethodNotAllowedError", - "MetricsResponse", + "MetricsV2Response", "Model", "ModelPrice", "ModelUsageUnit", "NotFoundError", "NumericScore", "NumericScoreV1", + "NumericScoreV3", "Observation", "ObservationBody", "ObservationLevel", "ObservationType", - "Observations", + "ObservationV2", + "ObservationsV2Meta", + "ObservationsV2Response", "ObservationsView", - "ObservationsViews", "OpenAiCompletionUsageSchema", "OpenAiResponseUsageSchema", "OpenAiUsage", "OptionalObservationBody", + "Organization", + "OrganizationApiKey", + "OrganizationApiKeysResponse", "OrganizationProject", "OrganizationProjectsResponse", + "OtelAttribute", + "OtelAttributeValue", + "OtelResource", + "OtelResourceSpan", + "OtelScope", + "OtelScopeSpan", + "OtelSpan", + "OtelTraceResponse", "PaginatedAnnotationQueueItems", "PaginatedAnnotationQueues", "PaginatedDatasetItems", "PaginatedDatasetRunItems", "PaginatedDatasetRuns", "PaginatedDatasets", + "PaginatedLlmConnections", "PaginatedModels", "PaginatedSessions", "PatchMediaBody", "PlaceholderMessage", + "PlaceholderMessageType", + "PricingTier", + "PricingTierCondition", + "PricingTierInput", + "PricingTierOperator", "Project", "ProjectDeletionResponse", "Projects", "Prompt", "PromptMeta", "PromptMetaListResponse", + "PromptType", "Prompt_Chat", "Prompt_Text", "ResourceMeta", @@ -365,17 +849,36 @@ "Score", "ScoreBody", "ScoreConfig", + "ScoreConfigDataType", "ScoreConfigs", "ScoreDataType", "ScoreEvent", "ScoreSource", + "ScoreSubjectExperimentV3", + "ScoreSubjectObservationV3", + "ScoreSubjectSessionV3", + "ScoreSubjectTraceV3", + "ScoreSubjectV3", + "ScoreSubjectV3_Experiment", + "ScoreSubjectV3_Observation", + "ScoreSubjectV3_Session", + "ScoreSubjectV3_Trace", "ScoreV1", "ScoreV1_Boolean", "ScoreV1_Categorical", "ScoreV1_Numeric", + "ScoreV1_Text", + "ScoreV3", + "ScoreV3_Boolean", + "ScoreV3_Categorical", + "ScoreV3_Correction", + "ScoreV3_Numeric", + "ScoreV3_Text", "Score_Boolean", "Score_Categorical", + "Score_Correction", "Score_Numeric", + "Score_Text", "SdkLogBody", "SdkLogEvent", "ServiceProviderConfig", @@ -384,6 +887,9 @@ "SessionWithTraces", "Sort", "TextPrompt", + "TextScore", + "TextScoreV1", + "TextScoreV3", "Trace", "TraceBody", "TraceEvent", @@ -396,12 +902,15 @@ "UpdateGenerationBody", "UpdateGenerationEvent", "UpdateObservationEvent", + "UpdateScoreConfigRequest", "UpdateSpanBody", "UpdateSpanEvent", + "UpsertLlmConnectionRequest", "Usage", "UsageDetails", "UserMeta", "annotation_queues", + "blob_storage_integrations", "comments", "commons", "dataset_items", @@ -409,19 +918,23 @@ "datasets", "health", "ingestion", + "legacy", + "llm_connections", "media", "metrics", "models", "observations", + "opentelemetry", "organizations", "projects", "prompt_version", "prompts", "scim", - "score", "score_configs", - "score_v_2", + "scores", + "scores_v3", "sessions", "trace", + "unstable", "utils", ] diff --git a/langfuse/api/annotation_queues/__init__.py b/langfuse/api/annotation_queues/__init__.py new file mode 100644 index 000000000..119661e05 --- /dev/null +++ b/langfuse/api/annotation_queues/__init__.py @@ -0,0 +1,82 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + AnnotationQueue, + AnnotationQueueAssignmentRequest, + AnnotationQueueItem, + AnnotationQueueObjectType, + AnnotationQueueStatus, + CreateAnnotationQueueAssignmentResponse, + CreateAnnotationQueueItemRequest, + CreateAnnotationQueueRequest, + DeleteAnnotationQueueAssignmentResponse, + DeleteAnnotationQueueItemResponse, + PaginatedAnnotationQueueItems, + PaginatedAnnotationQueues, + UpdateAnnotationQueueItemRequest, + ) +_dynamic_imports: typing.Dict[str, str] = { + "AnnotationQueue": ".types", + "AnnotationQueueAssignmentRequest": ".types", + "AnnotationQueueItem": ".types", + "AnnotationQueueObjectType": ".types", + "AnnotationQueueStatus": ".types", + "CreateAnnotationQueueAssignmentResponse": ".types", + "CreateAnnotationQueueItemRequest": ".types", + "CreateAnnotationQueueRequest": ".types", + "DeleteAnnotationQueueAssignmentResponse": ".types", + "DeleteAnnotationQueueItemResponse": ".types", + "PaginatedAnnotationQueueItems": ".types", + "PaginatedAnnotationQueues": ".types", + "UpdateAnnotationQueueItemRequest": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AnnotationQueue", + "AnnotationQueueAssignmentRequest", + "AnnotationQueueItem", + "AnnotationQueueObjectType", + "AnnotationQueueStatus", + "CreateAnnotationQueueAssignmentResponse", + "CreateAnnotationQueueItemRequest", + "CreateAnnotationQueueRequest", + "DeleteAnnotationQueueAssignmentResponse", + "DeleteAnnotationQueueItemResponse", + "PaginatedAnnotationQueueItems", + "PaginatedAnnotationQueues", + "UpdateAnnotationQueueItemRequest", +] diff --git a/langfuse/api/annotation_queues/client.py b/langfuse/api/annotation_queues/client.py new file mode 100644 index 000000000..6fb3d5682 --- /dev/null +++ b/langfuse/api/annotation_queues/client.py @@ -0,0 +1,1111 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawAnnotationQueuesClient, RawAnnotationQueuesClient +from .types.annotation_queue import AnnotationQueue +from .types.annotation_queue_item import AnnotationQueueItem +from .types.annotation_queue_object_type import AnnotationQueueObjectType +from .types.annotation_queue_status import AnnotationQueueStatus +from .types.create_annotation_queue_assignment_response import ( + CreateAnnotationQueueAssignmentResponse, +) +from .types.delete_annotation_queue_assignment_response import ( + DeleteAnnotationQueueAssignmentResponse, +) +from .types.delete_annotation_queue_item_response import ( + DeleteAnnotationQueueItemResponse, +) +from .types.paginated_annotation_queue_items import PaginatedAnnotationQueueItems +from .types.paginated_annotation_queues import PaginatedAnnotationQueues + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class AnnotationQueuesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawAnnotationQueuesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawAnnotationQueuesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawAnnotationQueuesClient + """ + return self._raw_client + + def list_queues( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedAnnotationQueues: + """ + Get all annotation queues + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedAnnotationQueues + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.list_queues() + """ + _response = self._raw_client.list_queues( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + def create_queue( + self, + *, + name: str, + score_config_ids: typing.Sequence[str], + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AnnotationQueue: + """ + Create an annotation queue + + Parameters + ---------- + name : str + + score_config_ids : typing.Sequence[str] + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueue + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.create_queue( + name="name", + score_config_ids=["scoreConfigIds", "scoreConfigIds"], + ) + """ + _response = self._raw_client.create_queue( + name=name, + score_config_ids=score_config_ids, + description=description, + request_options=request_options, + ) + return _response.data + + def get_queue( + self, queue_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AnnotationQueue: + """ + Get an annotation queue by ID + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueue + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.get_queue( + queue_id="queueId", + ) + """ + _response = self._raw_client.get_queue( + queue_id, request_options=request_options + ) + return _response.data + + def list_queue_items( + self, + queue_id: str, + *, + status: typing.Optional[AnnotationQueueStatus] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedAnnotationQueueItems: + """ + Get items for a specific annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + status : typing.Optional[AnnotationQueueStatus] + Filter by status + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedAnnotationQueueItems + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.list_queue_items( + queue_id="queueId", + ) + """ + _response = self._raw_client.list_queue_items( + queue_id, + status=status, + page=page, + limit=limit, + request_options=request_options, + ) + return _response.data + + def get_queue_item( + self, + queue_id: str, + item_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AnnotationQueueItem: + """ + Get a specific item from an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueueItem + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.get_queue_item( + queue_id="queueId", + item_id="itemId", + ) + """ + _response = self._raw_client.get_queue_item( + queue_id, item_id, request_options=request_options + ) + return _response.data + + def create_queue_item( + self, + queue_id: str, + *, + object_id: str, + object_type: AnnotationQueueObjectType, + status: typing.Optional[AnnotationQueueStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AnnotationQueueItem: + """ + Add an item to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + object_id : str + + object_type : AnnotationQueueObjectType + + status : typing.Optional[AnnotationQueueStatus] + Defaults to PENDING for new queue items + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueueItem + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.annotation_queues import AnnotationQueueObjectType + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.create_queue_item( + queue_id="queueId", + object_id="objectId", + object_type=AnnotationQueueObjectType.TRACE, + ) + """ + _response = self._raw_client.create_queue_item( + queue_id, + object_id=object_id, + object_type=object_type, + status=status, + request_options=request_options, + ) + return _response.data + + def update_queue_item( + self, + queue_id: str, + item_id: str, + *, + status: typing.Optional[AnnotationQueueStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AnnotationQueueItem: + """ + Update an annotation queue item + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + status : typing.Optional[AnnotationQueueStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueueItem + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.update_queue_item( + queue_id="queueId", + item_id="itemId", + ) + """ + _response = self._raw_client.update_queue_item( + queue_id, item_id, status=status, request_options=request_options + ) + return _response.data + + def delete_queue_item( + self, + queue_id: str, + item_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteAnnotationQueueItemResponse: + """ + Remove an item from an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteAnnotationQueueItemResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.delete_queue_item( + queue_id="queueId", + item_id="itemId", + ) + """ + _response = self._raw_client.delete_queue_item( + queue_id, item_id, request_options=request_options + ) + return _response.data + + def create_queue_assignment( + self, + queue_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateAnnotationQueueAssignmentResponse: + """ + Create an assignment for a user to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateAnnotationQueueAssignmentResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.create_queue_assignment( + queue_id="queueId", + user_id="userId", + ) + """ + _response = self._raw_client.create_queue_assignment( + queue_id, user_id=user_id, request_options=request_options + ) + return _response.data + + def delete_queue_assignment( + self, + queue_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteAnnotationQueueAssignmentResponse: + """ + Delete an assignment for a user to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteAnnotationQueueAssignmentResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.annotation_queues.delete_queue_assignment( + queue_id="queueId", + user_id="userId", + ) + """ + _response = self._raw_client.delete_queue_assignment( + queue_id, user_id=user_id, request_options=request_options + ) + return _response.data + + +class AsyncAnnotationQueuesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawAnnotationQueuesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawAnnotationQueuesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawAnnotationQueuesClient + """ + return self._raw_client + + async def list_queues( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedAnnotationQueues: + """ + Get all annotation queues + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedAnnotationQueues + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.list_queues() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_queues( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def create_queue( + self, + *, + name: str, + score_config_ids: typing.Sequence[str], + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AnnotationQueue: + """ + Create an annotation queue + + Parameters + ---------- + name : str + + score_config_ids : typing.Sequence[str] + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueue + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.create_queue( + name="name", + score_config_ids=["scoreConfigIds", "scoreConfigIds"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_queue( + name=name, + score_config_ids=score_config_ids, + description=description, + request_options=request_options, + ) + return _response.data + + async def get_queue( + self, queue_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AnnotationQueue: + """ + Get an annotation queue by ID + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueue + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.get_queue( + queue_id="queueId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_queue( + queue_id, request_options=request_options + ) + return _response.data + + async def list_queue_items( + self, + queue_id: str, + *, + status: typing.Optional[AnnotationQueueStatus] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedAnnotationQueueItems: + """ + Get items for a specific annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + status : typing.Optional[AnnotationQueueStatus] + Filter by status + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedAnnotationQueueItems + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.list_queue_items( + queue_id="queueId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_queue_items( + queue_id, + status=status, + page=page, + limit=limit, + request_options=request_options, + ) + return _response.data + + async def get_queue_item( + self, + queue_id: str, + item_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AnnotationQueueItem: + """ + Get a specific item from an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueueItem + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.get_queue_item( + queue_id="queueId", + item_id="itemId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_queue_item( + queue_id, item_id, request_options=request_options + ) + return _response.data + + async def create_queue_item( + self, + queue_id: str, + *, + object_id: str, + object_type: AnnotationQueueObjectType, + status: typing.Optional[AnnotationQueueStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AnnotationQueueItem: + """ + Add an item to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + object_id : str + + object_type : AnnotationQueueObjectType + + status : typing.Optional[AnnotationQueueStatus] + Defaults to PENDING for new queue items + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueueItem + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.annotation_queues import AnnotationQueueObjectType + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.create_queue_item( + queue_id="queueId", + object_id="objectId", + object_type=AnnotationQueueObjectType.TRACE, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_queue_item( + queue_id, + object_id=object_id, + object_type=object_type, + status=status, + request_options=request_options, + ) + return _response.data + + async def update_queue_item( + self, + queue_id: str, + item_id: str, + *, + status: typing.Optional[AnnotationQueueStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AnnotationQueueItem: + """ + Update an annotation queue item + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + status : typing.Optional[AnnotationQueueStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AnnotationQueueItem + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.update_queue_item( + queue_id="queueId", + item_id="itemId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_queue_item( + queue_id, item_id, status=status, request_options=request_options + ) + return _response.data + + async def delete_queue_item( + self, + queue_id: str, + item_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteAnnotationQueueItemResponse: + """ + Remove an item from an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteAnnotationQueueItemResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.delete_queue_item( + queue_id="queueId", + item_id="itemId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_queue_item( + queue_id, item_id, request_options=request_options + ) + return _response.data + + async def create_queue_assignment( + self, + queue_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateAnnotationQueueAssignmentResponse: + """ + Create an assignment for a user to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateAnnotationQueueAssignmentResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.create_queue_assignment( + queue_id="queueId", + user_id="userId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_queue_assignment( + queue_id, user_id=user_id, request_options=request_options + ) + return _response.data + + async def delete_queue_assignment( + self, + queue_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteAnnotationQueueAssignmentResponse: + """ + Delete an assignment for a user to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteAnnotationQueueAssignmentResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.annotation_queues.delete_queue_assignment( + queue_id="queueId", + user_id="userId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_queue_assignment( + queue_id, user_id=user_id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/annotation_queues/raw_client.py b/langfuse/api/annotation_queues/raw_client.py new file mode 100644 index 000000000..451095061 --- /dev/null +++ b/langfuse/api/annotation_queues/raw_client.py @@ -0,0 +1,2288 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.annotation_queue import AnnotationQueue +from .types.annotation_queue_item import AnnotationQueueItem +from .types.annotation_queue_object_type import AnnotationQueueObjectType +from .types.annotation_queue_status import AnnotationQueueStatus +from .types.create_annotation_queue_assignment_response import ( + CreateAnnotationQueueAssignmentResponse, +) +from .types.delete_annotation_queue_assignment_response import ( + DeleteAnnotationQueueAssignmentResponse, +) +from .types.delete_annotation_queue_item_response import ( + DeleteAnnotationQueueItemResponse, +) +from .types.paginated_annotation_queue_items import PaginatedAnnotationQueueItems +from .types.paginated_annotation_queues import PaginatedAnnotationQueues + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawAnnotationQueuesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_queues( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PaginatedAnnotationQueues]: + """ + Get all annotation queues + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedAnnotationQueues] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/annotation-queues", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedAnnotationQueues, + parse_obj_as( + type_=PaginatedAnnotationQueues, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create_queue( + self, + *, + name: str, + score_config_ids: typing.Sequence[str], + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[AnnotationQueue]: + """ + Create an annotation queue + + Parameters + ---------- + name : str + + score_config_ids : typing.Sequence[str] + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[AnnotationQueue] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/annotation-queues", + method="POST", + json={ + "name": name, + "description": description, + "scoreConfigIds": score_config_ids, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueue, + parse_obj_as( + type_=AnnotationQueue, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_queue( + self, queue_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[AnnotationQueue]: + """ + Get an annotation queue by ID + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[AnnotationQueue] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueue, + parse_obj_as( + type_=AnnotationQueue, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def list_queue_items( + self, + queue_id: str, + *, + status: typing.Optional[AnnotationQueueStatus] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PaginatedAnnotationQueueItems]: + """ + Get items for a specific annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + status : typing.Optional[AnnotationQueueStatus] + Filter by status + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedAnnotationQueueItems] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items", + method="GET", + params={ + "status": status, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedAnnotationQueueItems, + parse_obj_as( + type_=PaginatedAnnotationQueueItems, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_queue_item( + self, + queue_id: str, + item_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[AnnotationQueueItem]: + """ + Get a specific item from an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[AnnotationQueueItem] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueueItem, + parse_obj_as( + type_=AnnotationQueueItem, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create_queue_item( + self, + queue_id: str, + *, + object_id: str, + object_type: AnnotationQueueObjectType, + status: typing.Optional[AnnotationQueueStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[AnnotationQueueItem]: + """ + Add an item to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + object_id : str + + object_type : AnnotationQueueObjectType + + status : typing.Optional[AnnotationQueueStatus] + Defaults to PENDING for new queue items + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[AnnotationQueueItem] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items", + method="POST", + json={ + "objectId": object_id, + "objectType": object_type, + "status": status, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueueItem, + parse_obj_as( + type_=AnnotationQueueItem, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def update_queue_item( + self, + queue_id: str, + item_id: str, + *, + status: typing.Optional[AnnotationQueueStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[AnnotationQueueItem]: + """ + Update an annotation queue item + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + status : typing.Optional[AnnotationQueueStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[AnnotationQueueItem] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", + method="PATCH", + json={ + "status": status, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueueItem, + parse_obj_as( + type_=AnnotationQueueItem, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_queue_item( + self, + queue_id: str, + item_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DeleteAnnotationQueueItemResponse]: + """ + Remove an item from an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteAnnotationQueueItemResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteAnnotationQueueItemResponse, + parse_obj_as( + type_=DeleteAnnotationQueueItemResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create_queue_assignment( + self, + queue_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[CreateAnnotationQueueAssignmentResponse]: + """ + Create an assignment for a user to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[CreateAnnotationQueueAssignmentResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/assignments", + method="POST", + json={ + "userId": user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateAnnotationQueueAssignmentResponse, + parse_obj_as( + type_=CreateAnnotationQueueAssignmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_queue_assignment( + self, + queue_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DeleteAnnotationQueueAssignmentResponse]: + """ + Delete an assignment for a user to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteAnnotationQueueAssignmentResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/assignments", + method="DELETE", + json={ + "userId": user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteAnnotationQueueAssignmentResponse, + parse_obj_as( + type_=DeleteAnnotationQueueAssignmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawAnnotationQueuesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_queues( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PaginatedAnnotationQueues]: + """ + Get all annotation queues + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedAnnotationQueues] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/annotation-queues", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedAnnotationQueues, + parse_obj_as( + type_=PaginatedAnnotationQueues, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create_queue( + self, + *, + name: str, + score_config_ids: typing.Sequence[str], + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[AnnotationQueue]: + """ + Create an annotation queue + + Parameters + ---------- + name : str + + score_config_ids : typing.Sequence[str] + + description : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[AnnotationQueue] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/annotation-queues", + method="POST", + json={ + "name": name, + "description": description, + "scoreConfigIds": score_config_ids, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueue, + parse_obj_as( + type_=AnnotationQueue, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_queue( + self, queue_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[AnnotationQueue]: + """ + Get an annotation queue by ID + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[AnnotationQueue] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueue, + parse_obj_as( + type_=AnnotationQueue, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def list_queue_items( + self, + queue_id: str, + *, + status: typing.Optional[AnnotationQueueStatus] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PaginatedAnnotationQueueItems]: + """ + Get items for a specific annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + status : typing.Optional[AnnotationQueueStatus] + Filter by status + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedAnnotationQueueItems] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items", + method="GET", + params={ + "status": status, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedAnnotationQueueItems, + parse_obj_as( + type_=PaginatedAnnotationQueueItems, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_queue_item( + self, + queue_id: str, + item_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[AnnotationQueueItem]: + """ + Get a specific item from an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[AnnotationQueueItem] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueueItem, + parse_obj_as( + type_=AnnotationQueueItem, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create_queue_item( + self, + queue_id: str, + *, + object_id: str, + object_type: AnnotationQueueObjectType, + status: typing.Optional[AnnotationQueueStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[AnnotationQueueItem]: + """ + Add an item to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + object_id : str + + object_type : AnnotationQueueObjectType + + status : typing.Optional[AnnotationQueueStatus] + Defaults to PENDING for new queue items + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[AnnotationQueueItem] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items", + method="POST", + json={ + "objectId": object_id, + "objectType": object_type, + "status": status, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueueItem, + parse_obj_as( + type_=AnnotationQueueItem, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def update_queue_item( + self, + queue_id: str, + item_id: str, + *, + status: typing.Optional[AnnotationQueueStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[AnnotationQueueItem]: + """ + Update an annotation queue item + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + status : typing.Optional[AnnotationQueueStatus] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[AnnotationQueueItem] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", + method="PATCH", + json={ + "status": status, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + AnnotationQueueItem, + parse_obj_as( + type_=AnnotationQueueItem, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_queue_item( + self, + queue_id: str, + item_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DeleteAnnotationQueueItemResponse]: + """ + Remove an item from an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + item_id : str + The unique identifier of the annotation queue item + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteAnnotationQueueItemResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteAnnotationQueueItemResponse, + parse_obj_as( + type_=DeleteAnnotationQueueItemResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create_queue_assignment( + self, + queue_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[CreateAnnotationQueueAssignmentResponse]: + """ + Create an assignment for a user to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[CreateAnnotationQueueAssignmentResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/assignments", + method="POST", + json={ + "userId": user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateAnnotationQueueAssignmentResponse, + parse_obj_as( + type_=CreateAnnotationQueueAssignmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_queue_assignment( + self, + queue_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DeleteAnnotationQueueAssignmentResponse]: + """ + Delete an assignment for a user to an annotation queue + + Parameters + ---------- + queue_id : str + The unique identifier of the annotation queue + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteAnnotationQueueAssignmentResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/assignments", + method="DELETE", + json={ + "userId": user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteAnnotationQueueAssignmentResponse, + parse_obj_as( + type_=DeleteAnnotationQueueAssignmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/annotation_queues/types/__init__.py b/langfuse/api/annotation_queues/types/__init__.py new file mode 100644 index 000000000..0d34bb763 --- /dev/null +++ b/langfuse/api/annotation_queues/types/__init__.py @@ -0,0 +1,84 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .annotation_queue import AnnotationQueue + from .annotation_queue_assignment_request import AnnotationQueueAssignmentRequest + from .annotation_queue_item import AnnotationQueueItem + from .annotation_queue_object_type import AnnotationQueueObjectType + from .annotation_queue_status import AnnotationQueueStatus + from .create_annotation_queue_assignment_response import ( + CreateAnnotationQueueAssignmentResponse, + ) + from .create_annotation_queue_item_request import CreateAnnotationQueueItemRequest + from .create_annotation_queue_request import CreateAnnotationQueueRequest + from .delete_annotation_queue_assignment_response import ( + DeleteAnnotationQueueAssignmentResponse, + ) + from .delete_annotation_queue_item_response import DeleteAnnotationQueueItemResponse + from .paginated_annotation_queue_items import PaginatedAnnotationQueueItems + from .paginated_annotation_queues import PaginatedAnnotationQueues + from .update_annotation_queue_item_request import UpdateAnnotationQueueItemRequest +_dynamic_imports: typing.Dict[str, str] = { + "AnnotationQueue": ".annotation_queue", + "AnnotationQueueAssignmentRequest": ".annotation_queue_assignment_request", + "AnnotationQueueItem": ".annotation_queue_item", + "AnnotationQueueObjectType": ".annotation_queue_object_type", + "AnnotationQueueStatus": ".annotation_queue_status", + "CreateAnnotationQueueAssignmentResponse": ".create_annotation_queue_assignment_response", + "CreateAnnotationQueueItemRequest": ".create_annotation_queue_item_request", + "CreateAnnotationQueueRequest": ".create_annotation_queue_request", + "DeleteAnnotationQueueAssignmentResponse": ".delete_annotation_queue_assignment_response", + "DeleteAnnotationQueueItemResponse": ".delete_annotation_queue_item_response", + "PaginatedAnnotationQueueItems": ".paginated_annotation_queue_items", + "PaginatedAnnotationQueues": ".paginated_annotation_queues", + "UpdateAnnotationQueueItemRequest": ".update_annotation_queue_item_request", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AnnotationQueue", + "AnnotationQueueAssignmentRequest", + "AnnotationQueueItem", + "AnnotationQueueObjectType", + "AnnotationQueueStatus", + "CreateAnnotationQueueAssignmentResponse", + "CreateAnnotationQueueItemRequest", + "CreateAnnotationQueueRequest", + "DeleteAnnotationQueueAssignmentResponse", + "DeleteAnnotationQueueItemResponse", + "PaginatedAnnotationQueueItems", + "PaginatedAnnotationQueues", + "UpdateAnnotationQueueItemRequest", +] diff --git a/langfuse/api/annotation_queues/types/annotation_queue.py b/langfuse/api/annotation_queues/types/annotation_queue.py new file mode 100644 index 000000000..89cc5d407 --- /dev/null +++ b/langfuse/api/annotation_queues/types/annotation_queue.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class AnnotationQueue(UniversalBaseModel): + id: str + name: str + description: typing.Optional[str] = None + score_config_ids: typing_extensions.Annotated[ + typing.List[str], FieldMetadata(alias="scoreConfigIds") + ] + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/annotation_queue_assignment_request.py b/langfuse/api/annotation_queues/types/annotation_queue_assignment_request.py new file mode 100644 index 000000000..e25e4a327 --- /dev/null +++ b/langfuse/api/annotation_queues/types/annotation_queue_assignment_request.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class AnnotationQueueAssignmentRequest(UniversalBaseModel): + user_id: typing_extensions.Annotated[str, FieldMetadata(alias="userId")] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/annotation_queue_item.py b/langfuse/api/annotation_queues/types/annotation_queue_item.py new file mode 100644 index 000000000..9c4b622d8 --- /dev/null +++ b/langfuse/api/annotation_queues/types/annotation_queue_item.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .annotation_queue_object_type import AnnotationQueueObjectType +from .annotation_queue_status import AnnotationQueueStatus + + +class AnnotationQueueItem(UniversalBaseModel): + id: str + queue_id: typing_extensions.Annotated[str, FieldMetadata(alias="queueId")] + object_id: typing_extensions.Annotated[str, FieldMetadata(alias="objectId")] + object_type: typing_extensions.Annotated[ + AnnotationQueueObjectType, FieldMetadata(alias="objectType") + ] + status: AnnotationQueueStatus + completed_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="completedAt") + ] = None + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/resources/annotation_queues/types/annotation_queue_object_type.py b/langfuse/api/annotation_queues/types/annotation_queue_object_type.py similarity index 68% rename from langfuse/api/resources/annotation_queues/types/annotation_queue_object_type.py rename to langfuse/api/annotation_queues/types/annotation_queue_object_type.py index 1bef33b55..af8b95e89 100644 --- a/langfuse/api/resources/annotation_queues/types/annotation_queue_object_type.py +++ b/langfuse/api/annotation_queues/types/annotation_queue_object_type.py @@ -1,21 +1,26 @@ # This file was auto-generated by Fern from our API Definition. -import enum import typing +from ...core import enum + T_Result = typing.TypeVar("T_Result") -class AnnotationQueueObjectType(str, enum.Enum): +class AnnotationQueueObjectType(enum.StrEnum): TRACE = "TRACE" OBSERVATION = "OBSERVATION" + SESSION = "SESSION" def visit( self, trace: typing.Callable[[], T_Result], observation: typing.Callable[[], T_Result], + session: typing.Callable[[], T_Result], ) -> T_Result: if self is AnnotationQueueObjectType.TRACE: return trace() if self is AnnotationQueueObjectType.OBSERVATION: return observation() + if self is AnnotationQueueObjectType.SESSION: + return session() diff --git a/langfuse/api/resources/annotation_queues/types/annotation_queue_status.py b/langfuse/api/annotation_queues/types/annotation_queue_status.py similarity index 87% rename from langfuse/api/resources/annotation_queues/types/annotation_queue_status.py rename to langfuse/api/annotation_queues/types/annotation_queue_status.py index cf075f38a..a11fe3ea8 100644 --- a/langfuse/api/resources/annotation_queues/types/annotation_queue_status.py +++ b/langfuse/api/annotation_queues/types/annotation_queue_status.py @@ -1,12 +1,13 @@ # This file was auto-generated by Fern from our API Definition. -import enum import typing +from ...core import enum + T_Result = typing.TypeVar("T_Result") -class AnnotationQueueStatus(str, enum.Enum): +class AnnotationQueueStatus(enum.StrEnum): PENDING = "PENDING" COMPLETED = "COMPLETED" diff --git a/langfuse/api/annotation_queues/types/create_annotation_queue_assignment_response.py b/langfuse/api/annotation_queues/types/create_annotation_queue_assignment_response.py new file mode 100644 index 000000000..8f040d3e4 --- /dev/null +++ b/langfuse/api/annotation_queues/types/create_annotation_queue_assignment_response.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class CreateAnnotationQueueAssignmentResponse(UniversalBaseModel): + user_id: typing_extensions.Annotated[str, FieldMetadata(alias="userId")] + queue_id: typing_extensions.Annotated[str, FieldMetadata(alias="queueId")] + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/create_annotation_queue_item_request.py b/langfuse/api/annotation_queues/types/create_annotation_queue_item_request.py new file mode 100644 index 000000000..b81287ce5 --- /dev/null +++ b/langfuse/api/annotation_queues/types/create_annotation_queue_item_request.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .annotation_queue_object_type import AnnotationQueueObjectType +from .annotation_queue_status import AnnotationQueueStatus + + +class CreateAnnotationQueueItemRequest(UniversalBaseModel): + object_id: typing_extensions.Annotated[str, FieldMetadata(alias="objectId")] + object_type: typing_extensions.Annotated[ + AnnotationQueueObjectType, FieldMetadata(alias="objectType") + ] + status: typing.Optional[AnnotationQueueStatus] = pydantic.Field(default=None) + """ + Defaults to PENDING for new queue items + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/create_annotation_queue_request.py b/langfuse/api/annotation_queues/types/create_annotation_queue_request.py new file mode 100644 index 000000000..1415ad1a7 --- /dev/null +++ b/langfuse/api/annotation_queues/types/create_annotation_queue_request.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class CreateAnnotationQueueRequest(UniversalBaseModel): + name: str + description: typing.Optional[str] = None + score_config_ids: typing_extensions.Annotated[ + typing.List[str], FieldMetadata(alias="scoreConfigIds") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/delete_annotation_queue_assignment_response.py b/langfuse/api/annotation_queues/types/delete_annotation_queue_assignment_response.py new file mode 100644 index 000000000..547e2847e --- /dev/null +++ b/langfuse/api/annotation_queues/types/delete_annotation_queue_assignment_response.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class DeleteAnnotationQueueAssignmentResponse(UniversalBaseModel): + success: bool + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/delete_annotation_queue_item_response.py b/langfuse/api/annotation_queues/types/delete_annotation_queue_item_response.py new file mode 100644 index 000000000..ededb3a49 --- /dev/null +++ b/langfuse/api/annotation_queues/types/delete_annotation_queue_item_response.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class DeleteAnnotationQueueItemResponse(UniversalBaseModel): + success: bool + message: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/paginated_annotation_queue_items.py b/langfuse/api/annotation_queues/types/paginated_annotation_queue_items.py new file mode 100644 index 000000000..140d2efb9 --- /dev/null +++ b/langfuse/api/annotation_queues/types/paginated_annotation_queue_items.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse +from .annotation_queue_item import AnnotationQueueItem + + +class PaginatedAnnotationQueueItems(UniversalBaseModel): + data: typing.List[AnnotationQueueItem] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/paginated_annotation_queues.py b/langfuse/api/annotation_queues/types/paginated_annotation_queues.py new file mode 100644 index 000000000..e07a71ac7 --- /dev/null +++ b/langfuse/api/annotation_queues/types/paginated_annotation_queues.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse +from .annotation_queue import AnnotationQueue + + +class PaginatedAnnotationQueues(UniversalBaseModel): + data: typing.List[AnnotationQueue] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/annotation_queues/types/update_annotation_queue_item_request.py b/langfuse/api/annotation_queues/types/update_annotation_queue_item_request.py new file mode 100644 index 000000000..8f2c2f898 --- /dev/null +++ b/langfuse/api/annotation_queues/types/update_annotation_queue_item_request.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .annotation_queue_status import AnnotationQueueStatus + + +class UpdateAnnotationQueueItemRequest(UniversalBaseModel): + status: typing.Optional[AnnotationQueueStatus] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/blob_storage_integrations/__init__.py b/langfuse/api/blob_storage_integrations/__init__.py new file mode 100644 index 000000000..d92046ef2 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/__init__.py @@ -0,0 +1,79 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + BlobStorageExportFieldGroup, + BlobStorageExportFrequency, + BlobStorageExportMode, + BlobStorageExportSource, + BlobStorageIntegrationDeletionResponse, + BlobStorageIntegrationFileType, + BlobStorageIntegrationResponse, + BlobStorageIntegrationStatusResponse, + BlobStorageIntegrationType, + BlobStorageIntegrationsResponse, + BlobStorageSyncStatus, + CreateBlobStorageIntegrationRequest, + ) +_dynamic_imports: typing.Dict[str, str] = { + "BlobStorageExportFieldGroup": ".types", + "BlobStorageExportFrequency": ".types", + "BlobStorageExportMode": ".types", + "BlobStorageExportSource": ".types", + "BlobStorageIntegrationDeletionResponse": ".types", + "BlobStorageIntegrationFileType": ".types", + "BlobStorageIntegrationResponse": ".types", + "BlobStorageIntegrationStatusResponse": ".types", + "BlobStorageIntegrationType": ".types", + "BlobStorageIntegrationsResponse": ".types", + "BlobStorageSyncStatus": ".types", + "CreateBlobStorageIntegrationRequest": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BlobStorageExportFieldGroup", + "BlobStorageExportFrequency", + "BlobStorageExportMode", + "BlobStorageExportSource", + "BlobStorageIntegrationDeletionResponse", + "BlobStorageIntegrationFileType", + "BlobStorageIntegrationResponse", + "BlobStorageIntegrationStatusResponse", + "BlobStorageIntegrationType", + "BlobStorageIntegrationsResponse", + "BlobStorageSyncStatus", + "CreateBlobStorageIntegrationRequest", +] diff --git a/langfuse/api/blob_storage_integrations/client.py b/langfuse/api/blob_storage_integrations/client.py new file mode 100644 index 000000000..609e83fd3 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/client.py @@ -0,0 +1,602 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import ( + AsyncRawBlobStorageIntegrationsClient, + RawBlobStorageIntegrationsClient, +) +from .types.blob_storage_export_field_group import BlobStorageExportFieldGroup +from .types.blob_storage_export_frequency import BlobStorageExportFrequency +from .types.blob_storage_export_mode import BlobStorageExportMode +from .types.blob_storage_export_source import BlobStorageExportSource +from .types.blob_storage_integration_deletion_response import ( + BlobStorageIntegrationDeletionResponse, +) +from .types.blob_storage_integration_file_type import BlobStorageIntegrationFileType +from .types.blob_storage_integration_response import BlobStorageIntegrationResponse +from .types.blob_storage_integration_status_response import ( + BlobStorageIntegrationStatusResponse, +) +from .types.blob_storage_integration_type import BlobStorageIntegrationType +from .types.blob_storage_integrations_response import BlobStorageIntegrationsResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class BlobStorageIntegrationsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawBlobStorageIntegrationsClient( + client_wrapper=client_wrapper + ) + + @property + def with_raw_response(self) -> RawBlobStorageIntegrationsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawBlobStorageIntegrationsClient + """ + return self._raw_client + + def get_blob_storage_integrations( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> BlobStorageIntegrationsResponse: + """ + Get all blob storage integrations for the organization (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BlobStorageIntegrationsResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.blob_storage_integrations.get_blob_storage_integrations() + """ + _response = self._raw_client.get_blob_storage_integrations( + request_options=request_options + ) + return _response.data + + def upsert_blob_storage_integration( + self, + *, + project_id: str, + type: BlobStorageIntegrationType, + bucket_name: str, + region: str, + export_frequency: BlobStorageExportFrequency, + enabled: bool, + force_path_style: bool, + file_type: BlobStorageIntegrationFileType, + export_mode: BlobStorageExportMode, + endpoint: typing.Optional[str] = OMIT, + access_key_id: typing.Optional[str] = OMIT, + secret_access_key: typing.Optional[str] = OMIT, + prefix: typing.Optional[str] = OMIT, + export_start_date: typing.Optional[dt.datetime] = OMIT, + compressed: typing.Optional[bool] = OMIT, + export_source: typing.Optional[BlobStorageExportSource] = OMIT, + export_field_groups: typing.Optional[ + typing.Sequence[BlobStorageExportFieldGroup] + ] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> BlobStorageIntegrationResponse: + """ + Create or update a blob storage integration for a specific project (requires organization-scoped API key). The configuration is validated by performing a test upload to the bucket. + + Parameters + ---------- + project_id : str + ID of the project in which to configure the blob storage integration + + type : BlobStorageIntegrationType + + bucket_name : str + Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens only, must start and end with a letter or number, no consecutive hyphens). + + region : str + Storage region + + export_frequency : BlobStorageExportFrequency + + enabled : bool + Whether the integration is active + + force_path_style : bool + Use path-style URLs for S3 requests + + file_type : BlobStorageIntegrationFileType + + export_mode : BlobStorageExportMode + + endpoint : typing.Optional[str] + Custom endpoint URL (required for S3_COMPATIBLE type) + + access_key_id : typing.Optional[str] + Access key ID for authentication + + secret_access_key : typing.Optional[str] + Secret access key for authentication (will be encrypted when stored) + + prefix : typing.Optional[str] + Path prefix for exported files (must end with forward slash if provided) + + export_start_date : typing.Optional[dt.datetime] + Custom start date for exports (required when exportMode is FROM_CUSTOM_DATE) + + compressed : typing.Optional[bool] + Enable gzip compression for exported files (.csv.gz, .json.gz, .jsonl.gz). Defaults to true. + + export_source : typing.Optional[BlobStorageExportSource] + Data to export. When omitted on update, the existing value is preserved. When omitted on create: pre-cutoff Cloud projects and self-hosted deployments fall back to `LEGACY_TRACES_OBSERVATIONS`; post-cutoff Cloud projects (created on or after 2026-05-20) auto-default to `OBSERVATIONS_V2`. Required when `exportFieldGroups` is provided. + + **Cloud-only deprecation gate (effective 2026-05-20):** For projects created on or after 2026-05-20 on Langfuse Cloud, `LEGACY_TRACES_OBSERVATIONS` and `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` are rejected with HTTP 400. Omitting `exportSource` on these projects silently defaults to `OBSERVATIONS_V2` rather than the schema column default. Use `OBSERVATIONS_V2` for all new integrations. Projects created before 2026-05-20 and self-hosted deployments are unaffected. + + export_field_groups : typing.Optional[typing.Sequence[BlobStorageExportFieldGroup]] + Field groups to include in each exported row. + + For exportSource `OBSERVATIONS_V2` or `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`: must include `core` if provided. When omitted on create, the column default (all groups) applies. When omitted on update, the existing value is preserved. + + For exportSource `LEGACY_TRACES_OBSERVATIONS`: this field must be omitted or null. Sending an array (including an empty array) returns 400, because that source uses a fixed column set and does not honor field groups. + + `exportFieldGroups` requires `exportSource` to be provided in the same request. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BlobStorageIntegrationResponse + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.blob_storage_integrations import ( + BlobStorageExportFrequency, + BlobStorageExportMode, + BlobStorageIntegrationFileType, + BlobStorageIntegrationType, + ) + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.blob_storage_integrations.upsert_blob_storage_integration( + project_id="projectId", + type=BlobStorageIntegrationType.S3, + bucket_name="bucketName", + region="region", + export_frequency=BlobStorageExportFrequency.EVERY20MINUTES, + enabled=True, + force_path_style=True, + file_type=BlobStorageIntegrationFileType.JSON, + export_mode=BlobStorageExportMode.FULL_HISTORY, + ) + """ + _response = self._raw_client.upsert_blob_storage_integration( + project_id=project_id, + type=type, + bucket_name=bucket_name, + region=region, + export_frequency=export_frequency, + enabled=enabled, + force_path_style=force_path_style, + file_type=file_type, + export_mode=export_mode, + endpoint=endpoint, + access_key_id=access_key_id, + secret_access_key=secret_access_key, + prefix=prefix, + export_start_date=export_start_date, + compressed=compressed, + export_source=export_source, + export_field_groups=export_field_groups, + request_options=request_options, + ) + return _response.data + + def get_blob_storage_integration_status( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> BlobStorageIntegrationStatusResponse: + """ + Get the sync status of a blob storage integration by integration ID (requires organization-scoped API key) + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BlobStorageIntegrationStatusResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.blob_storage_integrations.get_blob_storage_integration_status( + id="id", + ) + """ + _response = self._raw_client.get_blob_storage_integration_status( + id, request_options=request_options + ) + return _response.data + + def delete_blob_storage_integration( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> BlobStorageIntegrationDeletionResponse: + """ + Delete a blob storage integration by ID (requires organization-scoped API key) + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BlobStorageIntegrationDeletionResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.blob_storage_integrations.delete_blob_storage_integration( + id="id", + ) + """ + _response = self._raw_client.delete_blob_storage_integration( + id, request_options=request_options + ) + return _response.data + + +class AsyncBlobStorageIntegrationsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawBlobStorageIntegrationsClient( + client_wrapper=client_wrapper + ) + + @property + def with_raw_response(self) -> AsyncRawBlobStorageIntegrationsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawBlobStorageIntegrationsClient + """ + return self._raw_client + + async def get_blob_storage_integrations( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> BlobStorageIntegrationsResponse: + """ + Get all blob storage integrations for the organization (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BlobStorageIntegrationsResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.blob_storage_integrations.get_blob_storage_integrations() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_blob_storage_integrations( + request_options=request_options + ) + return _response.data + + async def upsert_blob_storage_integration( + self, + *, + project_id: str, + type: BlobStorageIntegrationType, + bucket_name: str, + region: str, + export_frequency: BlobStorageExportFrequency, + enabled: bool, + force_path_style: bool, + file_type: BlobStorageIntegrationFileType, + export_mode: BlobStorageExportMode, + endpoint: typing.Optional[str] = OMIT, + access_key_id: typing.Optional[str] = OMIT, + secret_access_key: typing.Optional[str] = OMIT, + prefix: typing.Optional[str] = OMIT, + export_start_date: typing.Optional[dt.datetime] = OMIT, + compressed: typing.Optional[bool] = OMIT, + export_source: typing.Optional[BlobStorageExportSource] = OMIT, + export_field_groups: typing.Optional[ + typing.Sequence[BlobStorageExportFieldGroup] + ] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> BlobStorageIntegrationResponse: + """ + Create or update a blob storage integration for a specific project (requires organization-scoped API key). The configuration is validated by performing a test upload to the bucket. + + Parameters + ---------- + project_id : str + ID of the project in which to configure the blob storage integration + + type : BlobStorageIntegrationType + + bucket_name : str + Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens only, must start and end with a letter or number, no consecutive hyphens). + + region : str + Storage region + + export_frequency : BlobStorageExportFrequency + + enabled : bool + Whether the integration is active + + force_path_style : bool + Use path-style URLs for S3 requests + + file_type : BlobStorageIntegrationFileType + + export_mode : BlobStorageExportMode + + endpoint : typing.Optional[str] + Custom endpoint URL (required for S3_COMPATIBLE type) + + access_key_id : typing.Optional[str] + Access key ID for authentication + + secret_access_key : typing.Optional[str] + Secret access key for authentication (will be encrypted when stored) + + prefix : typing.Optional[str] + Path prefix for exported files (must end with forward slash if provided) + + export_start_date : typing.Optional[dt.datetime] + Custom start date for exports (required when exportMode is FROM_CUSTOM_DATE) + + compressed : typing.Optional[bool] + Enable gzip compression for exported files (.csv.gz, .json.gz, .jsonl.gz). Defaults to true. + + export_source : typing.Optional[BlobStorageExportSource] + Data to export. When omitted on update, the existing value is preserved. When omitted on create: pre-cutoff Cloud projects and self-hosted deployments fall back to `LEGACY_TRACES_OBSERVATIONS`; post-cutoff Cloud projects (created on or after 2026-05-20) auto-default to `OBSERVATIONS_V2`. Required when `exportFieldGroups` is provided. + + **Cloud-only deprecation gate (effective 2026-05-20):** For projects created on or after 2026-05-20 on Langfuse Cloud, `LEGACY_TRACES_OBSERVATIONS` and `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` are rejected with HTTP 400. Omitting `exportSource` on these projects silently defaults to `OBSERVATIONS_V2` rather than the schema column default. Use `OBSERVATIONS_V2` for all new integrations. Projects created before 2026-05-20 and self-hosted deployments are unaffected. + + export_field_groups : typing.Optional[typing.Sequence[BlobStorageExportFieldGroup]] + Field groups to include in each exported row. + + For exportSource `OBSERVATIONS_V2` or `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`: must include `core` if provided. When omitted on create, the column default (all groups) applies. When omitted on update, the existing value is preserved. + + For exportSource `LEGACY_TRACES_OBSERVATIONS`: this field must be omitted or null. Sending an array (including an empty array) returns 400, because that source uses a fixed column set and does not honor field groups. + + `exportFieldGroups` requires `exportSource` to be provided in the same request. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BlobStorageIntegrationResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.blob_storage_integrations import ( + BlobStorageExportFrequency, + BlobStorageExportMode, + BlobStorageIntegrationFileType, + BlobStorageIntegrationType, + ) + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.blob_storage_integrations.upsert_blob_storage_integration( + project_id="projectId", + type=BlobStorageIntegrationType.S3, + bucket_name="bucketName", + region="region", + export_frequency=BlobStorageExportFrequency.EVERY20MINUTES, + enabled=True, + force_path_style=True, + file_type=BlobStorageIntegrationFileType.JSON, + export_mode=BlobStorageExportMode.FULL_HISTORY, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.upsert_blob_storage_integration( + project_id=project_id, + type=type, + bucket_name=bucket_name, + region=region, + export_frequency=export_frequency, + enabled=enabled, + force_path_style=force_path_style, + file_type=file_type, + export_mode=export_mode, + endpoint=endpoint, + access_key_id=access_key_id, + secret_access_key=secret_access_key, + prefix=prefix, + export_start_date=export_start_date, + compressed=compressed, + export_source=export_source, + export_field_groups=export_field_groups, + request_options=request_options, + ) + return _response.data + + async def get_blob_storage_integration_status( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> BlobStorageIntegrationStatusResponse: + """ + Get the sync status of a blob storage integration by integration ID (requires organization-scoped API key) + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BlobStorageIntegrationStatusResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.blob_storage_integrations.get_blob_storage_integration_status( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_blob_storage_integration_status( + id, request_options=request_options + ) + return _response.data + + async def delete_blob_storage_integration( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> BlobStorageIntegrationDeletionResponse: + """ + Delete a blob storage integration by ID (requires organization-scoped API key) + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BlobStorageIntegrationDeletionResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.blob_storage_integrations.delete_blob_storage_integration( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_blob_storage_integration( + id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/blob_storage_integrations/raw_client.py b/langfuse/api/blob_storage_integrations/raw_client.py new file mode 100644 index 000000000..09e036db6 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/raw_client.py @@ -0,0 +1,1028 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.blob_storage_export_field_group import BlobStorageExportFieldGroup +from .types.blob_storage_export_frequency import BlobStorageExportFrequency +from .types.blob_storage_export_mode import BlobStorageExportMode +from .types.blob_storage_export_source import BlobStorageExportSource +from .types.blob_storage_integration_deletion_response import ( + BlobStorageIntegrationDeletionResponse, +) +from .types.blob_storage_integration_file_type import BlobStorageIntegrationFileType +from .types.blob_storage_integration_response import BlobStorageIntegrationResponse +from .types.blob_storage_integration_status_response import ( + BlobStorageIntegrationStatusResponse, +) +from .types.blob_storage_integration_type import BlobStorageIntegrationType +from .types.blob_storage_integrations_response import BlobStorageIntegrationsResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawBlobStorageIntegrationsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_blob_storage_integrations( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BlobStorageIntegrationsResponse]: + """ + Get all blob storage integrations for the organization (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BlobStorageIntegrationsResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/integrations/blob-storage", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BlobStorageIntegrationsResponse, + parse_obj_as( + type_=BlobStorageIntegrationsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def upsert_blob_storage_integration( + self, + *, + project_id: str, + type: BlobStorageIntegrationType, + bucket_name: str, + region: str, + export_frequency: BlobStorageExportFrequency, + enabled: bool, + force_path_style: bool, + file_type: BlobStorageIntegrationFileType, + export_mode: BlobStorageExportMode, + endpoint: typing.Optional[str] = OMIT, + access_key_id: typing.Optional[str] = OMIT, + secret_access_key: typing.Optional[str] = OMIT, + prefix: typing.Optional[str] = OMIT, + export_start_date: typing.Optional[dt.datetime] = OMIT, + compressed: typing.Optional[bool] = OMIT, + export_source: typing.Optional[BlobStorageExportSource] = OMIT, + export_field_groups: typing.Optional[ + typing.Sequence[BlobStorageExportFieldGroup] + ] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[BlobStorageIntegrationResponse]: + """ + Create or update a blob storage integration for a specific project (requires organization-scoped API key). The configuration is validated by performing a test upload to the bucket. + + Parameters + ---------- + project_id : str + ID of the project in which to configure the blob storage integration + + type : BlobStorageIntegrationType + + bucket_name : str + Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens only, must start and end with a letter or number, no consecutive hyphens). + + region : str + Storage region + + export_frequency : BlobStorageExportFrequency + + enabled : bool + Whether the integration is active + + force_path_style : bool + Use path-style URLs for S3 requests + + file_type : BlobStorageIntegrationFileType + + export_mode : BlobStorageExportMode + + endpoint : typing.Optional[str] + Custom endpoint URL (required for S3_COMPATIBLE type) + + access_key_id : typing.Optional[str] + Access key ID for authentication + + secret_access_key : typing.Optional[str] + Secret access key for authentication (will be encrypted when stored) + + prefix : typing.Optional[str] + Path prefix for exported files (must end with forward slash if provided) + + export_start_date : typing.Optional[dt.datetime] + Custom start date for exports (required when exportMode is FROM_CUSTOM_DATE) + + compressed : typing.Optional[bool] + Enable gzip compression for exported files (.csv.gz, .json.gz, .jsonl.gz). Defaults to true. + + export_source : typing.Optional[BlobStorageExportSource] + Data to export. When omitted on update, the existing value is preserved. When omitted on create: pre-cutoff Cloud projects and self-hosted deployments fall back to `LEGACY_TRACES_OBSERVATIONS`; post-cutoff Cloud projects (created on or after 2026-05-20) auto-default to `OBSERVATIONS_V2`. Required when `exportFieldGroups` is provided. + + **Cloud-only deprecation gate (effective 2026-05-20):** For projects created on or after 2026-05-20 on Langfuse Cloud, `LEGACY_TRACES_OBSERVATIONS` and `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` are rejected with HTTP 400. Omitting `exportSource` on these projects silently defaults to `OBSERVATIONS_V2` rather than the schema column default. Use `OBSERVATIONS_V2` for all new integrations. Projects created before 2026-05-20 and self-hosted deployments are unaffected. + + export_field_groups : typing.Optional[typing.Sequence[BlobStorageExportFieldGroup]] + Field groups to include in each exported row. + + For exportSource `OBSERVATIONS_V2` or `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`: must include `core` if provided. When omitted on create, the column default (all groups) applies. When omitted on update, the existing value is preserved. + + For exportSource `LEGACY_TRACES_OBSERVATIONS`: this field must be omitted or null. Sending an array (including an empty array) returns 400, because that source uses a fixed column set and does not honor field groups. + + `exportFieldGroups` requires `exportSource` to be provided in the same request. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BlobStorageIntegrationResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/integrations/blob-storage", + method="PUT", + json={ + "projectId": project_id, + "type": type, + "bucketName": bucket_name, + "endpoint": endpoint, + "region": region, + "accessKeyId": access_key_id, + "secretAccessKey": secret_access_key, + "prefix": prefix, + "exportFrequency": export_frequency, + "enabled": enabled, + "forcePathStyle": force_path_style, + "fileType": file_type, + "exportMode": export_mode, + "exportStartDate": export_start_date, + "compressed": compressed, + "exportSource": export_source, + "exportFieldGroups": export_field_groups, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BlobStorageIntegrationResponse, + parse_obj_as( + type_=BlobStorageIntegrationResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_blob_storage_integration_status( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BlobStorageIntegrationStatusResponse]: + """ + Get the sync status of a blob storage integration by integration ID (requires organization-scoped API key) + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BlobStorageIntegrationStatusResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/integrations/blob-storage/{jsonable_encoder(id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BlobStorageIntegrationStatusResponse, + parse_obj_as( + type_=BlobStorageIntegrationStatusResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_blob_storage_integration( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BlobStorageIntegrationDeletionResponse]: + """ + Delete a blob storage integration by ID (requires organization-scoped API key) + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BlobStorageIntegrationDeletionResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/integrations/blob-storage/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BlobStorageIntegrationDeletionResponse, + parse_obj_as( + type_=BlobStorageIntegrationDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawBlobStorageIntegrationsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_blob_storage_integrations( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BlobStorageIntegrationsResponse]: + """ + Get all blob storage integrations for the organization (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BlobStorageIntegrationsResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/integrations/blob-storage", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BlobStorageIntegrationsResponse, + parse_obj_as( + type_=BlobStorageIntegrationsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def upsert_blob_storage_integration( + self, + *, + project_id: str, + type: BlobStorageIntegrationType, + bucket_name: str, + region: str, + export_frequency: BlobStorageExportFrequency, + enabled: bool, + force_path_style: bool, + file_type: BlobStorageIntegrationFileType, + export_mode: BlobStorageExportMode, + endpoint: typing.Optional[str] = OMIT, + access_key_id: typing.Optional[str] = OMIT, + secret_access_key: typing.Optional[str] = OMIT, + prefix: typing.Optional[str] = OMIT, + export_start_date: typing.Optional[dt.datetime] = OMIT, + compressed: typing.Optional[bool] = OMIT, + export_source: typing.Optional[BlobStorageExportSource] = OMIT, + export_field_groups: typing.Optional[ + typing.Sequence[BlobStorageExportFieldGroup] + ] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[BlobStorageIntegrationResponse]: + """ + Create or update a blob storage integration for a specific project (requires organization-scoped API key). The configuration is validated by performing a test upload to the bucket. + + Parameters + ---------- + project_id : str + ID of the project in which to configure the blob storage integration + + type : BlobStorageIntegrationType + + bucket_name : str + Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens only, must start and end with a letter or number, no consecutive hyphens). + + region : str + Storage region + + export_frequency : BlobStorageExportFrequency + + enabled : bool + Whether the integration is active + + force_path_style : bool + Use path-style URLs for S3 requests + + file_type : BlobStorageIntegrationFileType + + export_mode : BlobStorageExportMode + + endpoint : typing.Optional[str] + Custom endpoint URL (required for S3_COMPATIBLE type) + + access_key_id : typing.Optional[str] + Access key ID for authentication + + secret_access_key : typing.Optional[str] + Secret access key for authentication (will be encrypted when stored) + + prefix : typing.Optional[str] + Path prefix for exported files (must end with forward slash if provided) + + export_start_date : typing.Optional[dt.datetime] + Custom start date for exports (required when exportMode is FROM_CUSTOM_DATE) + + compressed : typing.Optional[bool] + Enable gzip compression for exported files (.csv.gz, .json.gz, .jsonl.gz). Defaults to true. + + export_source : typing.Optional[BlobStorageExportSource] + Data to export. When omitted on update, the existing value is preserved. When omitted on create: pre-cutoff Cloud projects and self-hosted deployments fall back to `LEGACY_TRACES_OBSERVATIONS`; post-cutoff Cloud projects (created on or after 2026-05-20) auto-default to `OBSERVATIONS_V2`. Required when `exportFieldGroups` is provided. + + **Cloud-only deprecation gate (effective 2026-05-20):** For projects created on or after 2026-05-20 on Langfuse Cloud, `LEGACY_TRACES_OBSERVATIONS` and `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` are rejected with HTTP 400. Omitting `exportSource` on these projects silently defaults to `OBSERVATIONS_V2` rather than the schema column default. Use `OBSERVATIONS_V2` for all new integrations. Projects created before 2026-05-20 and self-hosted deployments are unaffected. + + export_field_groups : typing.Optional[typing.Sequence[BlobStorageExportFieldGroup]] + Field groups to include in each exported row. + + For exportSource `OBSERVATIONS_V2` or `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`: must include `core` if provided. When omitted on create, the column default (all groups) applies. When omitted on update, the existing value is preserved. + + For exportSource `LEGACY_TRACES_OBSERVATIONS`: this field must be omitted or null. Sending an array (including an empty array) returns 400, because that source uses a fixed column set and does not honor field groups. + + `exportFieldGroups` requires `exportSource` to be provided in the same request. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BlobStorageIntegrationResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/integrations/blob-storage", + method="PUT", + json={ + "projectId": project_id, + "type": type, + "bucketName": bucket_name, + "endpoint": endpoint, + "region": region, + "accessKeyId": access_key_id, + "secretAccessKey": secret_access_key, + "prefix": prefix, + "exportFrequency": export_frequency, + "enabled": enabled, + "forcePathStyle": force_path_style, + "fileType": file_type, + "exportMode": export_mode, + "exportStartDate": export_start_date, + "compressed": compressed, + "exportSource": export_source, + "exportFieldGroups": export_field_groups, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BlobStorageIntegrationResponse, + parse_obj_as( + type_=BlobStorageIntegrationResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_blob_storage_integration_status( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BlobStorageIntegrationStatusResponse]: + """ + Get the sync status of a blob storage integration by integration ID (requires organization-scoped API key) + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BlobStorageIntegrationStatusResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/integrations/blob-storage/{jsonable_encoder(id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BlobStorageIntegrationStatusResponse, + parse_obj_as( + type_=BlobStorageIntegrationStatusResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_blob_storage_integration( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BlobStorageIntegrationDeletionResponse]: + """ + Delete a blob storage integration by ID (requires organization-scoped API key) + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BlobStorageIntegrationDeletionResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/integrations/blob-storage/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BlobStorageIntegrationDeletionResponse, + parse_obj_as( + type_=BlobStorageIntegrationDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/blob_storage_integrations/types/__init__.py b/langfuse/api/blob_storage_integrations/types/__init__.py new file mode 100644 index 000000000..3a2a0e1ec --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/__init__.py @@ -0,0 +1,83 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .blob_storage_export_field_group import BlobStorageExportFieldGroup + from .blob_storage_export_frequency import BlobStorageExportFrequency + from .blob_storage_export_mode import BlobStorageExportMode + from .blob_storage_export_source import BlobStorageExportSource + from .blob_storage_integration_deletion_response import ( + BlobStorageIntegrationDeletionResponse, + ) + from .blob_storage_integration_file_type import BlobStorageIntegrationFileType + from .blob_storage_integration_response import BlobStorageIntegrationResponse + from .blob_storage_integration_status_response import ( + BlobStorageIntegrationStatusResponse, + ) + from .blob_storage_integration_type import BlobStorageIntegrationType + from .blob_storage_integrations_response import BlobStorageIntegrationsResponse + from .blob_storage_sync_status import BlobStorageSyncStatus + from .create_blob_storage_integration_request import ( + CreateBlobStorageIntegrationRequest, + ) +_dynamic_imports: typing.Dict[str, str] = { + "BlobStorageExportFieldGroup": ".blob_storage_export_field_group", + "BlobStorageExportFrequency": ".blob_storage_export_frequency", + "BlobStorageExportMode": ".blob_storage_export_mode", + "BlobStorageExportSource": ".blob_storage_export_source", + "BlobStorageIntegrationDeletionResponse": ".blob_storage_integration_deletion_response", + "BlobStorageIntegrationFileType": ".blob_storage_integration_file_type", + "BlobStorageIntegrationResponse": ".blob_storage_integration_response", + "BlobStorageIntegrationStatusResponse": ".blob_storage_integration_status_response", + "BlobStorageIntegrationType": ".blob_storage_integration_type", + "BlobStorageIntegrationsResponse": ".blob_storage_integrations_response", + "BlobStorageSyncStatus": ".blob_storage_sync_status", + "CreateBlobStorageIntegrationRequest": ".create_blob_storage_integration_request", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BlobStorageExportFieldGroup", + "BlobStorageExportFrequency", + "BlobStorageExportMode", + "BlobStorageExportSource", + "BlobStorageIntegrationDeletionResponse", + "BlobStorageIntegrationFileType", + "BlobStorageIntegrationResponse", + "BlobStorageIntegrationStatusResponse", + "BlobStorageIntegrationType", + "BlobStorageIntegrationsResponse", + "BlobStorageSyncStatus", + "CreateBlobStorageIntegrationRequest", +] diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_export_field_group.py b/langfuse/api/blob_storage_integrations/types/blob_storage_export_field_group.py new file mode 100644 index 000000000..c21a9c3bb --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_export_field_group.py @@ -0,0 +1,62 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class BlobStorageExportFieldGroup(enum.StrEnum): + """ + Field group for the OBSERVATIONS_V2 and LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS export. + """ + + CORE = "core" + BASIC = "basic" + TIME = "time" + IO = "io" + METADATA = "metadata" + MODEL = "model" + USAGE = "usage" + PROMPT = "prompt" + METRICS = "metrics" + TOOLS = "tools" + TRACE_CONTEXT = "trace_context" + + def visit( + self, + core: typing.Callable[[], T_Result], + basic: typing.Callable[[], T_Result], + time: typing.Callable[[], T_Result], + io: typing.Callable[[], T_Result], + metadata: typing.Callable[[], T_Result], + model: typing.Callable[[], T_Result], + usage: typing.Callable[[], T_Result], + prompt: typing.Callable[[], T_Result], + metrics: typing.Callable[[], T_Result], + tools: typing.Callable[[], T_Result], + trace_context: typing.Callable[[], T_Result], + ) -> T_Result: + if self is BlobStorageExportFieldGroup.CORE: + return core() + if self is BlobStorageExportFieldGroup.BASIC: + return basic() + if self is BlobStorageExportFieldGroup.TIME: + return time() + if self is BlobStorageExportFieldGroup.IO: + return io() + if self is BlobStorageExportFieldGroup.METADATA: + return metadata() + if self is BlobStorageExportFieldGroup.MODEL: + return model() + if self is BlobStorageExportFieldGroup.USAGE: + return usage() + if self is BlobStorageExportFieldGroup.PROMPT: + return prompt() + if self is BlobStorageExportFieldGroup.METRICS: + return metrics() + if self is BlobStorageExportFieldGroup.TOOLS: + return tools() + if self is BlobStorageExportFieldGroup.TRACE_CONTEXT: + return trace_context() diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_export_frequency.py b/langfuse/api/blob_storage_integrations/types/blob_storage_export_frequency.py new file mode 100644 index 000000000..4799ecefb --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_export_frequency.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class BlobStorageExportFrequency(enum.StrEnum): + EVERY20MINUTES = "every_20_minutes" + HOURLY = "hourly" + DAILY = "daily" + WEEKLY = "weekly" + + def visit( + self, + every20minutes: typing.Callable[[], T_Result], + hourly: typing.Callable[[], T_Result], + daily: typing.Callable[[], T_Result], + weekly: typing.Callable[[], T_Result], + ) -> T_Result: + if self is BlobStorageExportFrequency.EVERY20MINUTES: + return every20minutes() + if self is BlobStorageExportFrequency.HOURLY: + return hourly() + if self is BlobStorageExportFrequency.DAILY: + return daily() + if self is BlobStorageExportFrequency.WEEKLY: + return weekly() diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_export_mode.py b/langfuse/api/blob_storage_integrations/types/blob_storage_export_mode.py new file mode 100644 index 000000000..d692c0fb9 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_export_mode.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class BlobStorageExportMode(enum.StrEnum): + FULL_HISTORY = "FULL_HISTORY" + FROM_TODAY = "FROM_TODAY" + FROM_CUSTOM_DATE = "FROM_CUSTOM_DATE" + + def visit( + self, + full_history: typing.Callable[[], T_Result], + from_today: typing.Callable[[], T_Result], + from_custom_date: typing.Callable[[], T_Result], + ) -> T_Result: + if self is BlobStorageExportMode.FULL_HISTORY: + return full_history() + if self is BlobStorageExportMode.FROM_TODAY: + return from_today() + if self is BlobStorageExportMode.FROM_CUSTOM_DATE: + return from_custom_date() diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_export_source.py b/langfuse/api/blob_storage_integrations/types/blob_storage_export_source.py new file mode 100644 index 000000000..1451473b4 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_export_source.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class BlobStorageExportSource(enum.StrEnum): + """ + What data the integration exports. + - `LEGACY_TRACES_OBSERVATIONS`: traces, observations, and scores tables with a fixed column set. The `exportFieldGroups` field is not applicable. + - `OBSERVATIONS_V2`: same data model as the `/api/public/v2/observations` endpoint, plus scores. Columns are controlled by `exportFieldGroups`. + - `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`: both sets. For the `OBSERVATIONS_V2` portion, columns are controlled by `exportFieldGroups`. + + **Note:** `OBSERVATIONS_V2` and the enriched-observations portion of `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` rely on the enriched observations table (Langfuse Fast Preview / v4), which is currently available on Langfuse Cloud only. See https://langfuse.com/docs/v4. + """ + + LEGACY_TRACES_OBSERVATIONS = "LEGACY_TRACES_OBSERVATIONS" + OBSERVATIONS_V2 = "OBSERVATIONS_V2" + LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS = "LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS" + + def visit( + self, + legacy_traces_observations: typing.Callable[[], T_Result], + observations_v2: typing.Callable[[], T_Result], + legacy_traces_and_enriched_observations: typing.Callable[[], T_Result], + ) -> T_Result: + if self is BlobStorageExportSource.LEGACY_TRACES_OBSERVATIONS: + return legacy_traces_observations() + if self is BlobStorageExportSource.OBSERVATIONS_V2: + return observations_v2() + if self is BlobStorageExportSource.LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS: + return legacy_traces_and_enriched_observations() diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_integration_deletion_response.py b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_deletion_response.py new file mode 100644 index 000000000..d0c8655b1 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_deletion_response.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class BlobStorageIntegrationDeletionResponse(UniversalBaseModel): + message: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_integration_file_type.py b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_file_type.py new file mode 100644 index 000000000..52998e5e4 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_file_type.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class BlobStorageIntegrationFileType(enum.StrEnum): + JSON = "JSON" + CSV = "CSV" + JSONL = "JSONL" + + def visit( + self, + json: typing.Callable[[], T_Result], + csv: typing.Callable[[], T_Result], + jsonl: typing.Callable[[], T_Result], + ) -> T_Result: + if self is BlobStorageIntegrationFileType.JSON: + return json() + if self is BlobStorageIntegrationFileType.CSV: + return csv() + if self is BlobStorageIntegrationFileType.JSONL: + return jsonl() diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_integration_response.py b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_response.py new file mode 100644 index 000000000..e2b5921a0 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_response.py @@ -0,0 +1,78 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .blob_storage_export_field_group import BlobStorageExportFieldGroup +from .blob_storage_export_frequency import BlobStorageExportFrequency +from .blob_storage_export_mode import BlobStorageExportMode +from .blob_storage_export_source import BlobStorageExportSource +from .blob_storage_integration_file_type import BlobStorageIntegrationFileType +from .blob_storage_integration_type import BlobStorageIntegrationType + + +class BlobStorageIntegrationResponse(UniversalBaseModel): + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + type: BlobStorageIntegrationType + bucket_name: typing_extensions.Annotated[str, FieldMetadata(alias="bucketName")] + endpoint: typing.Optional[str] = None + region: str + access_key_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="accessKeyId") + ] = None + prefix: str + export_frequency: typing_extensions.Annotated[ + BlobStorageExportFrequency, FieldMetadata(alias="exportFrequency") + ] + enabled: bool + force_path_style: typing_extensions.Annotated[ + bool, FieldMetadata(alias="forcePathStyle") + ] + file_type: typing_extensions.Annotated[ + BlobStorageIntegrationFileType, FieldMetadata(alias="fileType") + ] + export_mode: typing_extensions.Annotated[ + BlobStorageExportMode, FieldMetadata(alias="exportMode") + ] + export_start_date: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="exportStartDate") + ] = None + compressed: bool + export_source: typing_extensions.Annotated[ + BlobStorageExportSource, FieldMetadata(alias="exportSource") + ] + export_field_groups: typing_extensions.Annotated[ + typing.Optional[typing.List[BlobStorageExportFieldGroup]], + FieldMetadata(alias="exportFieldGroups"), + ] = pydantic.Field(default=None) + """ + Field groups included in each exported row for `OBSERVATIONS_V2` / `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` sources. Always `null` when exportSource is `LEGACY_TRACES_OBSERVATIONS` (the field does not apply to that source; any legacy DB value is hidden from the public surface). + """ + + next_sync_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="nextSyncAt") + ] = None + last_sync_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="lastSyncAt") + ] = None + last_error: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="lastError") + ] = None + last_error_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="lastErrorAt") + ] = None + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_integration_status_response.py b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_status_response.py new file mode 100644 index 000000000..951074990 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_status_response.py @@ -0,0 +1,50 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .blob_storage_sync_status import BlobStorageSyncStatus + + +class BlobStorageIntegrationStatusResponse(UniversalBaseModel): + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + sync_status: typing_extensions.Annotated[ + BlobStorageSyncStatus, FieldMetadata(alias="syncStatus") + ] + enabled: bool + last_sync_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="lastSyncAt") + ] = pydantic.Field(default=None) + """ + End of the last successfully exported time window. Compare against your ETL bookmark to determine if new data is available. Null if the integration has never synced. + """ + + next_sync_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="nextSyncAt") + ] = pydantic.Field(default=None) + """ + When the next export is scheduled. Null if no sync has occurred yet. + """ + + last_error: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="lastError") + ] = pydantic.Field(default=None) + """ + Raw error message from the storage provider (S3/Azure/GCS) if the last export failed. Cleared on successful export. + """ + + last_error_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="lastErrorAt") + ] = pydantic.Field(default=None) + """ + When the last error occurred. Cleared on successful export. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_integration_type.py b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_type.py new file mode 100644 index 000000000..66828a62d --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_type.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class BlobStorageIntegrationType(enum.StrEnum): + S3 = "S3" + S3COMPATIBLE = "S3_COMPATIBLE" + AZURE_BLOB_STORAGE = "AZURE_BLOB_STORAGE" + + def visit( + self, + s3: typing.Callable[[], T_Result], + s3compatible: typing.Callable[[], T_Result], + azure_blob_storage: typing.Callable[[], T_Result], + ) -> T_Result: + if self is BlobStorageIntegrationType.S3: + return s3() + if self is BlobStorageIntegrationType.S3COMPATIBLE: + return s3compatible() + if self is BlobStorageIntegrationType.AZURE_BLOB_STORAGE: + return azure_blob_storage() diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_integrations_response.py b/langfuse/api/blob_storage_integrations/types/blob_storage_integrations_response.py new file mode 100644 index 000000000..0f3f5cdd7 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_integrations_response.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .blob_storage_integration_response import BlobStorageIntegrationResponse + + +class BlobStorageIntegrationsResponse(UniversalBaseModel): + data: typing.List[BlobStorageIntegrationResponse] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_sync_status.py b/langfuse/api/blob_storage_integrations/types/blob_storage_sync_status.py new file mode 100644 index 000000000..559e41450 --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_sync_status.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class BlobStorageSyncStatus(enum.StrEnum): + """ + Sync status of the blob storage integration: + - `disabled` — integration is not enabled + - `error` — last export failed (see `lastError` for details) + - `idle` — enabled but has never exported yet + - `queued` — next export is overdue (`nextSyncAt` is in the past) and waiting to be picked up by the worker + - `up_to_date` — all available data has been exported; next export is scheduled for the future + + **ETL usage**: poll this endpoint and check for `up_to_date` status. Compare `lastSyncAt` against your + ETL bookmark to determine if new data is available. Note that exports run with a 20-minute lag buffer, + so `lastSyncAt` will always be at least 20 minutes behind real-time. + """ + + IDLE = "idle" + QUEUED = "queued" + UP_TO_DATE = "up_to_date" + DISABLED = "disabled" + ERROR = "error" + + def visit( + self, + idle: typing.Callable[[], T_Result], + queued: typing.Callable[[], T_Result], + up_to_date: typing.Callable[[], T_Result], + disabled: typing.Callable[[], T_Result], + error: typing.Callable[[], T_Result], + ) -> T_Result: + if self is BlobStorageSyncStatus.IDLE: + return idle() + if self is BlobStorageSyncStatus.QUEUED: + return queued() + if self is BlobStorageSyncStatus.UP_TO_DATE: + return up_to_date() + if self is BlobStorageSyncStatus.DISABLED: + return disabled() + if self is BlobStorageSyncStatus.ERROR: + return error() diff --git a/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py b/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py new file mode 100644 index 000000000..89c9bca4a --- /dev/null +++ b/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py @@ -0,0 +1,121 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .blob_storage_export_field_group import BlobStorageExportFieldGroup +from .blob_storage_export_frequency import BlobStorageExportFrequency +from .blob_storage_export_mode import BlobStorageExportMode +from .blob_storage_export_source import BlobStorageExportSource +from .blob_storage_integration_file_type import BlobStorageIntegrationFileType +from .blob_storage_integration_type import BlobStorageIntegrationType + + +class CreateBlobStorageIntegrationRequest(UniversalBaseModel): + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] = ( + pydantic.Field() + ) + """ + ID of the project in which to configure the blob storage integration + """ + + type: BlobStorageIntegrationType + bucket_name: typing_extensions.Annotated[str, FieldMetadata(alias="bucketName")] = ( + pydantic.Field() + ) + """ + Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens only, must start and end with a letter or number, no consecutive hyphens). + """ + + endpoint: typing.Optional[str] = pydantic.Field(default=None) + """ + Custom endpoint URL (required for S3_COMPATIBLE type) + """ + + region: str = pydantic.Field() + """ + Storage region + """ + + access_key_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="accessKeyId") + ] = pydantic.Field(default=None) + """ + Access key ID for authentication + """ + + secret_access_key: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="secretAccessKey") + ] = pydantic.Field(default=None) + """ + Secret access key for authentication (will be encrypted when stored) + """ + + prefix: typing.Optional[str] = pydantic.Field(default=None) + """ + Path prefix for exported files (must end with forward slash if provided) + """ + + export_frequency: typing_extensions.Annotated[ + BlobStorageExportFrequency, FieldMetadata(alias="exportFrequency") + ] + enabled: bool = pydantic.Field() + """ + Whether the integration is active + """ + + force_path_style: typing_extensions.Annotated[ + bool, FieldMetadata(alias="forcePathStyle") + ] = pydantic.Field() + """ + Use path-style URLs for S3 requests + """ + + file_type: typing_extensions.Annotated[ + BlobStorageIntegrationFileType, FieldMetadata(alias="fileType") + ] + export_mode: typing_extensions.Annotated[ + BlobStorageExportMode, FieldMetadata(alias="exportMode") + ] + export_start_date: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="exportStartDate") + ] = pydantic.Field(default=None) + """ + Custom start date for exports (required when exportMode is FROM_CUSTOM_DATE) + """ + + compressed: typing.Optional[bool] = pydantic.Field(default=None) + """ + Enable gzip compression for exported files (.csv.gz, .json.gz, .jsonl.gz). Defaults to true. + """ + + export_source: typing_extensions.Annotated[ + typing.Optional[BlobStorageExportSource], FieldMetadata(alias="exportSource") + ] = pydantic.Field(default=None) + """ + Data to export. When omitted on update, the existing value is preserved. When omitted on create: pre-cutoff Cloud projects and self-hosted deployments fall back to `LEGACY_TRACES_OBSERVATIONS`; post-cutoff Cloud projects (created on or after 2026-05-20) auto-default to `OBSERVATIONS_V2`. Required when `exportFieldGroups` is provided. + + **Cloud-only deprecation gate (effective 2026-05-20):** For projects created on or after 2026-05-20 on Langfuse Cloud, `LEGACY_TRACES_OBSERVATIONS` and `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS` are rejected with HTTP 400. Omitting `exportSource` on these projects silently defaults to `OBSERVATIONS_V2` rather than the schema column default. Use `OBSERVATIONS_V2` for all new integrations. Projects created before 2026-05-20 and self-hosted deployments are unaffected. + """ + + export_field_groups: typing_extensions.Annotated[ + typing.Optional[typing.List[BlobStorageExportFieldGroup]], + FieldMetadata(alias="exportFieldGroups"), + ] = pydantic.Field(default=None) + """ + Field groups to include in each exported row. + + For exportSource `OBSERVATIONS_V2` or `LEGACY_TRACES_AND_ENRICHED_OBSERVATIONS`: must include `core` if provided. When omitted on create, the column default (all groups) applies. When omitted on update, the existing value is preserved. + + For exportSource `LEGACY_TRACES_OBSERVATIONS`: this field must be omitted or null. Sending an array (including an empty array) returns 400, because that source uses a fixed column set and does not honor field groups. + + `exportFieldGroups` requires `exportSource` to be provided in the same request. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/client.py b/langfuse/api/client.py index 87b46c2f8..a72aede85 100644 --- a/langfuse/api/client.py +++ b/langfuse/api/client.py @@ -1,46 +1,51 @@ # This file was auto-generated by Fern from our API Definition. +from __future__ import annotations + import typing import httpx - from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from .resources.annotation_queues.client import ( - AnnotationQueuesClient, - AsyncAnnotationQueuesClient, -) -from .resources.comments.client import AsyncCommentsClient, CommentsClient -from .resources.dataset_items.client import AsyncDatasetItemsClient, DatasetItemsClient -from .resources.dataset_run_items.client import ( - AsyncDatasetRunItemsClient, - DatasetRunItemsClient, -) -from .resources.datasets.client import AsyncDatasetsClient, DatasetsClient -from .resources.health.client import AsyncHealthClient, HealthClient -from .resources.ingestion.client import AsyncIngestionClient, IngestionClient -from .resources.media.client import AsyncMediaClient, MediaClient -from .resources.metrics.client import AsyncMetricsClient, MetricsClient -from .resources.models.client import AsyncModelsClient, ModelsClient -from .resources.observations.client import AsyncObservationsClient, ObservationsClient -from .resources.organizations.client import ( - AsyncOrganizationsClient, - OrganizationsClient, -) -from .resources.projects.client import AsyncProjectsClient, ProjectsClient -from .resources.prompt_version.client import ( - AsyncPromptVersionClient, - PromptVersionClient, -) -from .resources.prompts.client import AsyncPromptsClient, PromptsClient -from .resources.scim.client import AsyncScimClient, ScimClient -from .resources.score.client import AsyncScoreClient, ScoreClient -from .resources.score_configs.client import AsyncScoreConfigsClient, ScoreConfigsClient -from .resources.score_v_2.client import AsyncScoreV2Client, ScoreV2Client -from .resources.sessions.client import AsyncSessionsClient, SessionsClient -from .resources.trace.client import AsyncTraceClient, TraceClient - - -class FernLangfuse: + +if typing.TYPE_CHECKING: + from .annotation_queues.client import ( + AnnotationQueuesClient, + AsyncAnnotationQueuesClient, + ) + from .blob_storage_integrations.client import ( + AsyncBlobStorageIntegrationsClient, + BlobStorageIntegrationsClient, + ) + from .comments.client import AsyncCommentsClient, CommentsClient + from .dataset_items.client import AsyncDatasetItemsClient, DatasetItemsClient + from .dataset_run_items.client import ( + AsyncDatasetRunItemsClient, + DatasetRunItemsClient, + ) + from .datasets.client import AsyncDatasetsClient, DatasetsClient + from .health.client import AsyncHealthClient, HealthClient + from .ingestion.client import AsyncIngestionClient, IngestionClient + from .legacy.client import AsyncLegacyClient, LegacyClient + from .llm_connections.client import AsyncLlmConnectionsClient, LlmConnectionsClient + from .media.client import AsyncMediaClient, MediaClient + from .metrics.client import AsyncMetricsClient, MetricsClient + from .models.client import AsyncModelsClient, ModelsClient + from .observations.client import AsyncObservationsClient, ObservationsClient + from .opentelemetry.client import AsyncOpentelemetryClient, OpentelemetryClient + from .organizations.client import AsyncOrganizationsClient, OrganizationsClient + from .projects.client import AsyncProjectsClient, ProjectsClient + from .prompt_version.client import AsyncPromptVersionClient, PromptVersionClient + from .prompts.client import AsyncPromptsClient, PromptsClient + from .scim.client import AsyncScimClient, ScimClient + from .score_configs.client import AsyncScoreConfigsClient, ScoreConfigsClient + from .scores.client import AsyncScoresClient, ScoresClient + from .scores_v3.client import AsyncScoresV3Client, ScoresV3Client + from .sessions.client import AsyncSessionsClient, SessionsClient + from .trace.client import AsyncTraceClient, TraceClient + from .unstable.client import AsyncUnstableClient, UnstableClient + + +class LangfuseAPI: """ Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. @@ -54,6 +59,9 @@ class FernLangfuse: x_langfuse_public_key : typing.Optional[str] username : typing.Optional[typing.Union[str, typing.Callable[[], str]]] password : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + timeout : typing.Optional[float] The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. @@ -65,9 +73,9 @@ class FernLangfuse: Examples -------- - from langfuse.client import FernLangfuse + from langfuse import LangfuseAPI - client = FernLangfuse( + client = LangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -86,12 +94,17 @@ def __init__( x_langfuse_public_key: typing.Optional[str] = None, username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, ): _defaulted_timeout = ( - timeout if timeout is not None else 60 if httpx_client is None else None + timeout + if timeout is not None + else 60 + if httpx_client is None + else httpx_client.timeout.read ) self._client_wrapper = SyncClientWrapper( base_url=base_url, @@ -100,6 +113,7 @@ def __init__( x_langfuse_public_key=x_langfuse_public_key, username=username, password=password, + headers=headers, httpx_client=httpx_client if httpx_client is not None else httpx.Client( @@ -109,34 +123,263 @@ def __init__( else httpx.Client(timeout=_defaulted_timeout), timeout=_defaulted_timeout, ) - self.annotation_queues = AnnotationQueuesClient( - client_wrapper=self._client_wrapper - ) - self.comments = CommentsClient(client_wrapper=self._client_wrapper) - self.dataset_items = DatasetItemsClient(client_wrapper=self._client_wrapper) - self.dataset_run_items = DatasetRunItemsClient( - client_wrapper=self._client_wrapper - ) - self.datasets = DatasetsClient(client_wrapper=self._client_wrapper) - self.health = HealthClient(client_wrapper=self._client_wrapper) - self.ingestion = IngestionClient(client_wrapper=self._client_wrapper) - self.media = MediaClient(client_wrapper=self._client_wrapper) - self.metrics = MetricsClient(client_wrapper=self._client_wrapper) - self.models = ModelsClient(client_wrapper=self._client_wrapper) - self.observations = ObservationsClient(client_wrapper=self._client_wrapper) - self.organizations = OrganizationsClient(client_wrapper=self._client_wrapper) - self.projects = ProjectsClient(client_wrapper=self._client_wrapper) - self.prompt_version = PromptVersionClient(client_wrapper=self._client_wrapper) - self.prompts = PromptsClient(client_wrapper=self._client_wrapper) - self.scim = ScimClient(client_wrapper=self._client_wrapper) - self.score_configs = ScoreConfigsClient(client_wrapper=self._client_wrapper) - self.score_v_2 = ScoreV2Client(client_wrapper=self._client_wrapper) - self.score = ScoreClient(client_wrapper=self._client_wrapper) - self.sessions = SessionsClient(client_wrapper=self._client_wrapper) - self.trace = TraceClient(client_wrapper=self._client_wrapper) - - -class AsyncFernLangfuse: + self._annotation_queues: typing.Optional[AnnotationQueuesClient] = None + self._blob_storage_integrations: typing.Optional[ + BlobStorageIntegrationsClient + ] = None + self._comments: typing.Optional[CommentsClient] = None + self._dataset_items: typing.Optional[DatasetItemsClient] = None + self._dataset_run_items: typing.Optional[DatasetRunItemsClient] = None + self._datasets: typing.Optional[DatasetsClient] = None + self._health: typing.Optional[HealthClient] = None + self._ingestion: typing.Optional[IngestionClient] = None + self._legacy: typing.Optional[LegacyClient] = None + self._llm_connections: typing.Optional[LlmConnectionsClient] = None + self._media: typing.Optional[MediaClient] = None + self._metrics: typing.Optional[MetricsClient] = None + self._models: typing.Optional[ModelsClient] = None + self._observations: typing.Optional[ObservationsClient] = None + self._opentelemetry: typing.Optional[OpentelemetryClient] = None + self._organizations: typing.Optional[OrganizationsClient] = None + self._projects: typing.Optional[ProjectsClient] = None + self._prompt_version: typing.Optional[PromptVersionClient] = None + self._prompts: typing.Optional[PromptsClient] = None + self._scim: typing.Optional[ScimClient] = None + self._score_configs: typing.Optional[ScoreConfigsClient] = None + self._scores_v3: typing.Optional[ScoresV3Client] = None + self._scores: typing.Optional[ScoresClient] = None + self._sessions: typing.Optional[SessionsClient] = None + self._trace: typing.Optional[TraceClient] = None + self._unstable: typing.Optional[UnstableClient] = None + + @property + def annotation_queues(self): + if self._annotation_queues is None: + from .annotation_queues.client import AnnotationQueuesClient # noqa: E402 + + self._annotation_queues = AnnotationQueuesClient( + client_wrapper=self._client_wrapper + ) + return self._annotation_queues + + @property + def blob_storage_integrations(self): + if self._blob_storage_integrations is None: + from .blob_storage_integrations.client import BlobStorageIntegrationsClient # noqa: E402 + + self._blob_storage_integrations = BlobStorageIntegrationsClient( + client_wrapper=self._client_wrapper + ) + return self._blob_storage_integrations + + @property + def comments(self): + if self._comments is None: + from .comments.client import CommentsClient # noqa: E402 + + self._comments = CommentsClient(client_wrapper=self._client_wrapper) + return self._comments + + @property + def dataset_items(self): + if self._dataset_items is None: + from .dataset_items.client import DatasetItemsClient # noqa: E402 + + self._dataset_items = DatasetItemsClient( + client_wrapper=self._client_wrapper + ) + return self._dataset_items + + @property + def dataset_run_items(self): + if self._dataset_run_items is None: + from .dataset_run_items.client import DatasetRunItemsClient # noqa: E402 + + self._dataset_run_items = DatasetRunItemsClient( + client_wrapper=self._client_wrapper + ) + return self._dataset_run_items + + @property + def datasets(self): + if self._datasets is None: + from .datasets.client import DatasetsClient # noqa: E402 + + self._datasets = DatasetsClient(client_wrapper=self._client_wrapper) + return self._datasets + + @property + def health(self): + if self._health is None: + from .health.client import HealthClient # noqa: E402 + + self._health = HealthClient(client_wrapper=self._client_wrapper) + return self._health + + @property + def ingestion(self): + if self._ingestion is None: + from .ingestion.client import IngestionClient # noqa: E402 + + self._ingestion = IngestionClient(client_wrapper=self._client_wrapper) + return self._ingestion + + @property + def legacy(self): + if self._legacy is None: + from .legacy.client import LegacyClient # noqa: E402 + + self._legacy = LegacyClient(client_wrapper=self._client_wrapper) + return self._legacy + + @property + def llm_connections(self): + if self._llm_connections is None: + from .llm_connections.client import LlmConnectionsClient # noqa: E402 + + self._llm_connections = LlmConnectionsClient( + client_wrapper=self._client_wrapper + ) + return self._llm_connections + + @property + def media(self): + if self._media is None: + from .media.client import MediaClient # noqa: E402 + + self._media = MediaClient(client_wrapper=self._client_wrapper) + return self._media + + @property + def metrics(self): + if self._metrics is None: + from .metrics.client import MetricsClient # noqa: E402 + + self._metrics = MetricsClient(client_wrapper=self._client_wrapper) + return self._metrics + + @property + def models(self): + if self._models is None: + from .models.client import ModelsClient # noqa: E402 + + self._models = ModelsClient(client_wrapper=self._client_wrapper) + return self._models + + @property + def observations(self): + if self._observations is None: + from .observations.client import ObservationsClient # noqa: E402 + + self._observations = ObservationsClient(client_wrapper=self._client_wrapper) + return self._observations + + @property + def opentelemetry(self): + if self._opentelemetry is None: + from .opentelemetry.client import OpentelemetryClient # noqa: E402 + + self._opentelemetry = OpentelemetryClient( + client_wrapper=self._client_wrapper + ) + return self._opentelemetry + + @property + def organizations(self): + if self._organizations is None: + from .organizations.client import OrganizationsClient # noqa: E402 + + self._organizations = OrganizationsClient( + client_wrapper=self._client_wrapper + ) + return self._organizations + + @property + def projects(self): + if self._projects is None: + from .projects.client import ProjectsClient # noqa: E402 + + self._projects = ProjectsClient(client_wrapper=self._client_wrapper) + return self._projects + + @property + def prompt_version(self): + if self._prompt_version is None: + from .prompt_version.client import PromptVersionClient # noqa: E402 + + self._prompt_version = PromptVersionClient( + client_wrapper=self._client_wrapper + ) + return self._prompt_version + + @property + def prompts(self): + if self._prompts is None: + from .prompts.client import PromptsClient # noqa: E402 + + self._prompts = PromptsClient(client_wrapper=self._client_wrapper) + return self._prompts + + @property + def scim(self): + if self._scim is None: + from .scim.client import ScimClient # noqa: E402 + + self._scim = ScimClient(client_wrapper=self._client_wrapper) + return self._scim + + @property + def score_configs(self): + if self._score_configs is None: + from .score_configs.client import ScoreConfigsClient # noqa: E402 + + self._score_configs = ScoreConfigsClient( + client_wrapper=self._client_wrapper + ) + return self._score_configs + + @property + def scores_v3(self): + if self._scores_v3 is None: + from .scores_v3.client import ScoresV3Client # noqa: E402 + + self._scores_v3 = ScoresV3Client(client_wrapper=self._client_wrapper) + return self._scores_v3 + + @property + def scores(self): + if self._scores is None: + from .scores.client import ScoresClient # noqa: E402 + + self._scores = ScoresClient(client_wrapper=self._client_wrapper) + return self._scores + + @property + def sessions(self): + if self._sessions is None: + from .sessions.client import SessionsClient # noqa: E402 + + self._sessions = SessionsClient(client_wrapper=self._client_wrapper) + return self._sessions + + @property + def trace(self): + if self._trace is None: + from .trace.client import TraceClient # noqa: E402 + + self._trace = TraceClient(client_wrapper=self._client_wrapper) + return self._trace + + @property + def unstable(self): + if self._unstable is None: + from .unstable.client import UnstableClient # noqa: E402 + + self._unstable = UnstableClient(client_wrapper=self._client_wrapper) + return self._unstable + + +class AsyncLangfuseAPI: """ Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. @@ -150,6 +393,9 @@ class AsyncFernLangfuse: x_langfuse_public_key : typing.Optional[str] username : typing.Optional[typing.Union[str, typing.Callable[[], str]]] password : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + timeout : typing.Optional[float] The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. @@ -161,9 +407,9 @@ class AsyncFernLangfuse: Examples -------- - from langfuse.client import AsyncFernLangfuse + from langfuse import AsyncLangfuseAPI - client = AsyncFernLangfuse( + client = AsyncLangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -182,12 +428,17 @@ def __init__( x_langfuse_public_key: typing.Optional[str] = None, username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, ): _defaulted_timeout = ( - timeout if timeout is not None else 60 if httpx_client is None else None + timeout + if timeout is not None + else 60 + if httpx_client is None + else httpx_client.timeout.read ) self._client_wrapper = AsyncClientWrapper( base_url=base_url, @@ -196,6 +447,7 @@ def __init__( x_langfuse_public_key=x_langfuse_public_key, username=username, password=password, + headers=headers, httpx_client=httpx_client if httpx_client is not None else httpx.AsyncClient( @@ -205,36 +457,261 @@ def __init__( else httpx.AsyncClient(timeout=_defaulted_timeout), timeout=_defaulted_timeout, ) - self.annotation_queues = AsyncAnnotationQueuesClient( - client_wrapper=self._client_wrapper - ) - self.comments = AsyncCommentsClient(client_wrapper=self._client_wrapper) - self.dataset_items = AsyncDatasetItemsClient( - client_wrapper=self._client_wrapper - ) - self.dataset_run_items = AsyncDatasetRunItemsClient( - client_wrapper=self._client_wrapper - ) - self.datasets = AsyncDatasetsClient(client_wrapper=self._client_wrapper) - self.health = AsyncHealthClient(client_wrapper=self._client_wrapper) - self.ingestion = AsyncIngestionClient(client_wrapper=self._client_wrapper) - self.media = AsyncMediaClient(client_wrapper=self._client_wrapper) - self.metrics = AsyncMetricsClient(client_wrapper=self._client_wrapper) - self.models = AsyncModelsClient(client_wrapper=self._client_wrapper) - self.observations = AsyncObservationsClient(client_wrapper=self._client_wrapper) - self.organizations = AsyncOrganizationsClient( - client_wrapper=self._client_wrapper - ) - self.projects = AsyncProjectsClient(client_wrapper=self._client_wrapper) - self.prompt_version = AsyncPromptVersionClient( - client_wrapper=self._client_wrapper - ) - self.prompts = AsyncPromptsClient(client_wrapper=self._client_wrapper) - self.scim = AsyncScimClient(client_wrapper=self._client_wrapper) - self.score_configs = AsyncScoreConfigsClient( - client_wrapper=self._client_wrapper - ) - self.score_v_2 = AsyncScoreV2Client(client_wrapper=self._client_wrapper) - self.score = AsyncScoreClient(client_wrapper=self._client_wrapper) - self.sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) - self.trace = AsyncTraceClient(client_wrapper=self._client_wrapper) + self._annotation_queues: typing.Optional[AsyncAnnotationQueuesClient] = None + self._blob_storage_integrations: typing.Optional[ + AsyncBlobStorageIntegrationsClient + ] = None + self._comments: typing.Optional[AsyncCommentsClient] = None + self._dataset_items: typing.Optional[AsyncDatasetItemsClient] = None + self._dataset_run_items: typing.Optional[AsyncDatasetRunItemsClient] = None + self._datasets: typing.Optional[AsyncDatasetsClient] = None + self._health: typing.Optional[AsyncHealthClient] = None + self._ingestion: typing.Optional[AsyncIngestionClient] = None + self._legacy: typing.Optional[AsyncLegacyClient] = None + self._llm_connections: typing.Optional[AsyncLlmConnectionsClient] = None + self._media: typing.Optional[AsyncMediaClient] = None + self._metrics: typing.Optional[AsyncMetricsClient] = None + self._models: typing.Optional[AsyncModelsClient] = None + self._observations: typing.Optional[AsyncObservationsClient] = None + self._opentelemetry: typing.Optional[AsyncOpentelemetryClient] = None + self._organizations: typing.Optional[AsyncOrganizationsClient] = None + self._projects: typing.Optional[AsyncProjectsClient] = None + self._prompt_version: typing.Optional[AsyncPromptVersionClient] = None + self._prompts: typing.Optional[AsyncPromptsClient] = None + self._scim: typing.Optional[AsyncScimClient] = None + self._score_configs: typing.Optional[AsyncScoreConfigsClient] = None + self._scores_v3: typing.Optional[AsyncScoresV3Client] = None + self._scores: typing.Optional[AsyncScoresClient] = None + self._sessions: typing.Optional[AsyncSessionsClient] = None + self._trace: typing.Optional[AsyncTraceClient] = None + self._unstable: typing.Optional[AsyncUnstableClient] = None + + @property + def annotation_queues(self): + if self._annotation_queues is None: + from .annotation_queues.client import AsyncAnnotationQueuesClient # noqa: E402 + + self._annotation_queues = AsyncAnnotationQueuesClient( + client_wrapper=self._client_wrapper + ) + return self._annotation_queues + + @property + def blob_storage_integrations(self): + if self._blob_storage_integrations is None: + from .blob_storage_integrations.client import ( + AsyncBlobStorageIntegrationsClient, + ) # noqa: E402 + + self._blob_storage_integrations = AsyncBlobStorageIntegrationsClient( + client_wrapper=self._client_wrapper + ) + return self._blob_storage_integrations + + @property + def comments(self): + if self._comments is None: + from .comments.client import AsyncCommentsClient # noqa: E402 + + self._comments = AsyncCommentsClient(client_wrapper=self._client_wrapper) + return self._comments + + @property + def dataset_items(self): + if self._dataset_items is None: + from .dataset_items.client import AsyncDatasetItemsClient # noqa: E402 + + self._dataset_items = AsyncDatasetItemsClient( + client_wrapper=self._client_wrapper + ) + return self._dataset_items + + @property + def dataset_run_items(self): + if self._dataset_run_items is None: + from .dataset_run_items.client import AsyncDatasetRunItemsClient # noqa: E402 + + self._dataset_run_items = AsyncDatasetRunItemsClient( + client_wrapper=self._client_wrapper + ) + return self._dataset_run_items + + @property + def datasets(self): + if self._datasets is None: + from .datasets.client import AsyncDatasetsClient # noqa: E402 + + self._datasets = AsyncDatasetsClient(client_wrapper=self._client_wrapper) + return self._datasets + + @property + def health(self): + if self._health is None: + from .health.client import AsyncHealthClient # noqa: E402 + + self._health = AsyncHealthClient(client_wrapper=self._client_wrapper) + return self._health + + @property + def ingestion(self): + if self._ingestion is None: + from .ingestion.client import AsyncIngestionClient # noqa: E402 + + self._ingestion = AsyncIngestionClient(client_wrapper=self._client_wrapper) + return self._ingestion + + @property + def legacy(self): + if self._legacy is None: + from .legacy.client import AsyncLegacyClient # noqa: E402 + + self._legacy = AsyncLegacyClient(client_wrapper=self._client_wrapper) + return self._legacy + + @property + def llm_connections(self): + if self._llm_connections is None: + from .llm_connections.client import AsyncLlmConnectionsClient # noqa: E402 + + self._llm_connections = AsyncLlmConnectionsClient( + client_wrapper=self._client_wrapper + ) + return self._llm_connections + + @property + def media(self): + if self._media is None: + from .media.client import AsyncMediaClient # noqa: E402 + + self._media = AsyncMediaClient(client_wrapper=self._client_wrapper) + return self._media + + @property + def metrics(self): + if self._metrics is None: + from .metrics.client import AsyncMetricsClient # noqa: E402 + + self._metrics = AsyncMetricsClient(client_wrapper=self._client_wrapper) + return self._metrics + + @property + def models(self): + if self._models is None: + from .models.client import AsyncModelsClient # noqa: E402 + + self._models = AsyncModelsClient(client_wrapper=self._client_wrapper) + return self._models + + @property + def observations(self): + if self._observations is None: + from .observations.client import AsyncObservationsClient # noqa: E402 + + self._observations = AsyncObservationsClient( + client_wrapper=self._client_wrapper + ) + return self._observations + + @property + def opentelemetry(self): + if self._opentelemetry is None: + from .opentelemetry.client import AsyncOpentelemetryClient # noqa: E402 + + self._opentelemetry = AsyncOpentelemetryClient( + client_wrapper=self._client_wrapper + ) + return self._opentelemetry + + @property + def organizations(self): + if self._organizations is None: + from .organizations.client import AsyncOrganizationsClient # noqa: E402 + + self._organizations = AsyncOrganizationsClient( + client_wrapper=self._client_wrapper + ) + return self._organizations + + @property + def projects(self): + if self._projects is None: + from .projects.client import AsyncProjectsClient # noqa: E402 + + self._projects = AsyncProjectsClient(client_wrapper=self._client_wrapper) + return self._projects + + @property + def prompt_version(self): + if self._prompt_version is None: + from .prompt_version.client import AsyncPromptVersionClient # noqa: E402 + + self._prompt_version = AsyncPromptVersionClient( + client_wrapper=self._client_wrapper + ) + return self._prompt_version + + @property + def prompts(self): + if self._prompts is None: + from .prompts.client import AsyncPromptsClient # noqa: E402 + + self._prompts = AsyncPromptsClient(client_wrapper=self._client_wrapper) + return self._prompts + + @property + def scim(self): + if self._scim is None: + from .scim.client import AsyncScimClient # noqa: E402 + + self._scim = AsyncScimClient(client_wrapper=self._client_wrapper) + return self._scim + + @property + def score_configs(self): + if self._score_configs is None: + from .score_configs.client import AsyncScoreConfigsClient # noqa: E402 + + self._score_configs = AsyncScoreConfigsClient( + client_wrapper=self._client_wrapper + ) + return self._score_configs + + @property + def scores_v3(self): + if self._scores_v3 is None: + from .scores_v3.client import AsyncScoresV3Client # noqa: E402 + + self._scores_v3 = AsyncScoresV3Client(client_wrapper=self._client_wrapper) + return self._scores_v3 + + @property + def scores(self): + if self._scores is None: + from .scores.client import AsyncScoresClient # noqa: E402 + + self._scores = AsyncScoresClient(client_wrapper=self._client_wrapper) + return self._scores + + @property + def sessions(self): + if self._sessions is None: + from .sessions.client import AsyncSessionsClient # noqa: E402 + + self._sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) + return self._sessions + + @property + def trace(self): + if self._trace is None: + from .trace.client import AsyncTraceClient # noqa: E402 + + self._trace = AsyncTraceClient(client_wrapper=self._client_wrapper) + return self._trace + + @property + def unstable(self): + if self._unstable is None: + from .unstable.client import AsyncUnstableClient # noqa: E402 + + self._unstable = AsyncUnstableClient(client_wrapper=self._client_wrapper) + return self._unstable diff --git a/langfuse/api/comments/__init__.py b/langfuse/api/comments/__init__.py new file mode 100644 index 000000000..0588586c7 --- /dev/null +++ b/langfuse/api/comments/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import CreateCommentRequest, CreateCommentResponse, GetCommentsResponse +_dynamic_imports: typing.Dict[str, str] = { + "CreateCommentRequest": ".types", + "CreateCommentResponse": ".types", + "GetCommentsResponse": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateCommentRequest", "CreateCommentResponse", "GetCommentsResponse"] diff --git a/langfuse/api/comments/client.py b/langfuse/api/comments/client.py new file mode 100644 index 000000000..f5e92ff36 --- /dev/null +++ b/langfuse/api/comments/client.py @@ -0,0 +1,407 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..commons.types.comment import Comment +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawCommentsClient, RawCommentsClient +from .types.create_comment_response import CreateCommentResponse +from .types.get_comments_response import GetCommentsResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class CommentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawCommentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawCommentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawCommentsClient + """ + return self._raw_client + + def create( + self, + *, + project_id: str, + object_type: str, + object_id: str, + content: str, + author_user_id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateCommentResponse: + """ + Create a comment. Comments may be attached to different object types (trace, observation, session, prompt). + + Parameters + ---------- + project_id : str + The id of the project to attach the comment to. + + object_type : str + The type of the object to attach the comment to (trace, observation, session, prompt). + + object_id : str + The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown. + + content : str + The content of the comment. May include markdown. Currently limited to 5000 characters. + + author_user_id : typing.Optional[str] + The id of the user who created the comment. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateCommentResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.comments.create( + project_id="projectId", + object_type="objectType", + object_id="objectId", + content="content", + ) + """ + _response = self._raw_client.create( + project_id=project_id, + object_type=object_type, + object_id=object_id, + content=content, + author_user_id=author_user_id, + request_options=request_options, + ) + return _response.data + + def get( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + object_type: typing.Optional[str] = None, + object_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetCommentsResponse: + """ + Get all comments + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit + + object_type : typing.Optional[str] + Filter comments by object type (trace, observation, session, prompt). + + object_id : typing.Optional[str] + Filter comments by object id. If objectType is not provided, an error will be thrown. + + author_user_id : typing.Optional[str] + Filter comments by author user id. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetCommentsResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.comments.get() + """ + _response = self._raw_client.get( + page=page, + limit=limit, + object_type=object_type, + object_id=object_id, + author_user_id=author_user_id, + request_options=request_options, + ) + return _response.data + + def get_by_id( + self, + comment_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> Comment: + """ + Get a comment by id + + Parameters + ---------- + comment_id : str + The unique langfuse identifier of a comment + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Comment + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.comments.get_by_id( + comment_id="commentId", + ) + """ + _response = self._raw_client.get_by_id( + comment_id, request_options=request_options + ) + return _response.data + + +class AsyncCommentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawCommentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawCommentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawCommentsClient + """ + return self._raw_client + + async def create( + self, + *, + project_id: str, + object_type: str, + object_id: str, + content: str, + author_user_id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateCommentResponse: + """ + Create a comment. Comments may be attached to different object types (trace, observation, session, prompt). + + Parameters + ---------- + project_id : str + The id of the project to attach the comment to. + + object_type : str + The type of the object to attach the comment to (trace, observation, session, prompt). + + object_id : str + The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown. + + content : str + The content of the comment. May include markdown. Currently limited to 5000 characters. + + author_user_id : typing.Optional[str] + The id of the user who created the comment. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateCommentResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.comments.create( + project_id="projectId", + object_type="objectType", + object_id="objectId", + content="content", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + project_id=project_id, + object_type=object_type, + object_id=object_id, + content=content, + author_user_id=author_user_id, + request_options=request_options, + ) + return _response.data + + async def get( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + object_type: typing.Optional[str] = None, + object_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetCommentsResponse: + """ + Get all comments + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit + + object_type : typing.Optional[str] + Filter comments by object type (trace, observation, session, prompt). + + object_id : typing.Optional[str] + Filter comments by object id. If objectType is not provided, an error will be thrown. + + author_user_id : typing.Optional[str] + Filter comments by author user id. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetCommentsResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.comments.get() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + page=page, + limit=limit, + object_type=object_type, + object_id=object_id, + author_user_id=author_user_id, + request_options=request_options, + ) + return _response.data + + async def get_by_id( + self, + comment_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> Comment: + """ + Get a comment by id + + Parameters + ---------- + comment_id : str + The unique langfuse identifier of a comment + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Comment + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.comments.get_by_id( + comment_id="commentId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_by_id( + comment_id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/comments/raw_client.py b/langfuse/api/comments/raw_client.py new file mode 100644 index 000000000..0bb39539a --- /dev/null +++ b/langfuse/api/comments/raw_client.py @@ -0,0 +1,750 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.comment import Comment +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.create_comment_response import CreateCommentResponse +from .types.get_comments_response import GetCommentsResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawCommentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + project_id: str, + object_type: str, + object_id: str, + content: str, + author_user_id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[CreateCommentResponse]: + """ + Create a comment. Comments may be attached to different object types (trace, observation, session, prompt). + + Parameters + ---------- + project_id : str + The id of the project to attach the comment to. + + object_type : str + The type of the object to attach the comment to (trace, observation, session, prompt). + + object_id : str + The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown. + + content : str + The content of the comment. May include markdown. Currently limited to 5000 characters. + + author_user_id : typing.Optional[str] + The id of the user who created the comment. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[CreateCommentResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/comments", + method="POST", + json={ + "projectId": project_id, + "objectType": object_type, + "objectId": object_id, + "content": content, + "authorUserId": author_user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateCommentResponse, + parse_obj_as( + type_=CreateCommentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + object_type: typing.Optional[str] = None, + object_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetCommentsResponse]: + """ + Get all comments + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit + + object_type : typing.Optional[str] + Filter comments by object type (trace, observation, session, prompt). + + object_id : typing.Optional[str] + Filter comments by object id. If objectType is not provided, an error will be thrown. + + author_user_id : typing.Optional[str] + Filter comments by author user id. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetCommentsResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/comments", + method="GET", + params={ + "page": page, + "limit": limit, + "objectType": object_type, + "objectId": object_id, + "authorUserId": author_user_id, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetCommentsResponse, + parse_obj_as( + type_=GetCommentsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_by_id( + self, + comment_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Comment]: + """ + Get a comment by id + + Parameters + ---------- + comment_id : str + The unique langfuse identifier of a comment + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Comment] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/comments/{jsonable_encoder(comment_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Comment, + parse_obj_as( + type_=Comment, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawCommentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + project_id: str, + object_type: str, + object_id: str, + content: str, + author_user_id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[CreateCommentResponse]: + """ + Create a comment. Comments may be attached to different object types (trace, observation, session, prompt). + + Parameters + ---------- + project_id : str + The id of the project to attach the comment to. + + object_type : str + The type of the object to attach the comment to (trace, observation, session, prompt). + + object_id : str + The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown. + + content : str + The content of the comment. May include markdown. Currently limited to 5000 characters. + + author_user_id : typing.Optional[str] + The id of the user who created the comment. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[CreateCommentResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/comments", + method="POST", + json={ + "projectId": project_id, + "objectType": object_type, + "objectId": object_id, + "content": content, + "authorUserId": author_user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateCommentResponse, + parse_obj_as( + type_=CreateCommentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + object_type: typing.Optional[str] = None, + object_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetCommentsResponse]: + """ + Get all comments + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit + + object_type : typing.Optional[str] + Filter comments by object type (trace, observation, session, prompt). + + object_id : typing.Optional[str] + Filter comments by object id. If objectType is not provided, an error will be thrown. + + author_user_id : typing.Optional[str] + Filter comments by author user id. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetCommentsResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/comments", + method="GET", + params={ + "page": page, + "limit": limit, + "objectType": object_type, + "objectId": object_id, + "authorUserId": author_user_id, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetCommentsResponse, + parse_obj_as( + type_=GetCommentsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_by_id( + self, + comment_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Comment]: + """ + Get a comment by id + + Parameters + ---------- + comment_id : str + The unique langfuse identifier of a comment + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Comment] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/comments/{jsonable_encoder(comment_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Comment, + parse_obj_as( + type_=Comment, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/comments/types/__init__.py b/langfuse/api/comments/types/__init__.py new file mode 100644 index 000000000..4936025a0 --- /dev/null +++ b/langfuse/api/comments/types/__init__.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_comment_request import CreateCommentRequest + from .create_comment_response import CreateCommentResponse + from .get_comments_response import GetCommentsResponse +_dynamic_imports: typing.Dict[str, str] = { + "CreateCommentRequest": ".create_comment_request", + "CreateCommentResponse": ".create_comment_response", + "GetCommentsResponse": ".get_comments_response", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateCommentRequest", "CreateCommentResponse", "GetCommentsResponse"] diff --git a/langfuse/api/comments/types/create_comment_request.py b/langfuse/api/comments/types/create_comment_request.py new file mode 100644 index 000000000..56ef2794d --- /dev/null +++ b/langfuse/api/comments/types/create_comment_request.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class CreateCommentRequest(UniversalBaseModel): + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] = ( + pydantic.Field() + ) + """ + The id of the project to attach the comment to. + """ + + object_type: typing_extensions.Annotated[str, FieldMetadata(alias="objectType")] = ( + pydantic.Field() + ) + """ + The type of the object to attach the comment to (trace, observation, session, prompt). + """ + + object_id: typing_extensions.Annotated[str, FieldMetadata(alias="objectId")] = ( + pydantic.Field() + ) + """ + The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown. + """ + + content: str = pydantic.Field() + """ + The content of the comment. May include markdown. Currently limited to 5000 characters. + """ + + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = pydantic.Field(default=None) + """ + The id of the user who created the comment. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/comments/types/create_comment_response.py b/langfuse/api/comments/types/create_comment_response.py new file mode 100644 index 000000000..d080349b0 --- /dev/null +++ b/langfuse/api/comments/types/create_comment_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class CreateCommentResponse(UniversalBaseModel): + id: str = pydantic.Field() + """ + The id of the created object in Langfuse + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/comments/types/get_comments_response.py b/langfuse/api/comments/types/get_comments_response.py new file mode 100644 index 000000000..f275210e8 --- /dev/null +++ b/langfuse/api/comments/types/get_comments_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.comment import Comment +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse + + +class GetCommentsResponse(UniversalBaseModel): + data: typing.List[Comment] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/__init__.py b/langfuse/api/commons/__init__.py new file mode 100644 index 000000000..81cb57f96 --- /dev/null +++ b/langfuse/api/commons/__init__.py @@ -0,0 +1,222 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + BaseScore, + BaseScoreV1, + BooleanScore, + BooleanScoreV1, + CategoricalScore, + CategoricalScoreV1, + Comment, + CommentObjectType, + ConfigCategory, + CorrectionScore, + CreateScoreValue, + Dataset, + DatasetItem, + DatasetRun, + DatasetRunItem, + DatasetRunWithItems, + DatasetStatus, + MapValue, + Model, + ModelPrice, + ModelUsageUnit, + NumericScore, + NumericScoreV1, + Observation, + ObservationLevel, + ObservationV2, + ObservationsView, + PricingTier, + PricingTierCondition, + PricingTierInput, + PricingTierOperator, + Score, + ScoreConfig, + ScoreConfigDataType, + ScoreDataType, + ScoreSource, + ScoreV1, + ScoreV1_Boolean, + ScoreV1_Categorical, + ScoreV1_Numeric, + ScoreV1_Text, + Score_Boolean, + Score_Categorical, + Score_Correction, + Score_Numeric, + Score_Text, + Session, + SessionWithTraces, + TextScore, + TextScoreV1, + Trace, + TraceWithDetails, + TraceWithFullDetails, + Usage, + ) + from .errors import ( + AccessDeniedError, + Error, + MethodNotAllowedError, + NotFoundError, + UnauthorizedError, + ) +_dynamic_imports: typing.Dict[str, str] = { + "AccessDeniedError": ".errors", + "BaseScore": ".types", + "BaseScoreV1": ".types", + "BooleanScore": ".types", + "BooleanScoreV1": ".types", + "CategoricalScore": ".types", + "CategoricalScoreV1": ".types", + "Comment": ".types", + "CommentObjectType": ".types", + "ConfigCategory": ".types", + "CorrectionScore": ".types", + "CreateScoreValue": ".types", + "Dataset": ".types", + "DatasetItem": ".types", + "DatasetRun": ".types", + "DatasetRunItem": ".types", + "DatasetRunWithItems": ".types", + "DatasetStatus": ".types", + "Error": ".errors", + "MapValue": ".types", + "MethodNotAllowedError": ".errors", + "Model": ".types", + "ModelPrice": ".types", + "ModelUsageUnit": ".types", + "NotFoundError": ".errors", + "NumericScore": ".types", + "NumericScoreV1": ".types", + "Observation": ".types", + "ObservationLevel": ".types", + "ObservationV2": ".types", + "ObservationsView": ".types", + "PricingTier": ".types", + "PricingTierCondition": ".types", + "PricingTierInput": ".types", + "PricingTierOperator": ".types", + "Score": ".types", + "ScoreConfig": ".types", + "ScoreConfigDataType": ".types", + "ScoreDataType": ".types", + "ScoreSource": ".types", + "ScoreV1": ".types", + "ScoreV1_Boolean": ".types", + "ScoreV1_Categorical": ".types", + "ScoreV1_Numeric": ".types", + "ScoreV1_Text": ".types", + "Score_Boolean": ".types", + "Score_Categorical": ".types", + "Score_Correction": ".types", + "Score_Numeric": ".types", + "Score_Text": ".types", + "Session": ".types", + "SessionWithTraces": ".types", + "TextScore": ".types", + "TextScoreV1": ".types", + "Trace": ".types", + "TraceWithDetails": ".types", + "TraceWithFullDetails": ".types", + "UnauthorizedError": ".errors", + "Usage": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AccessDeniedError", + "BaseScore", + "BaseScoreV1", + "BooleanScore", + "BooleanScoreV1", + "CategoricalScore", + "CategoricalScoreV1", + "Comment", + "CommentObjectType", + "ConfigCategory", + "CorrectionScore", + "CreateScoreValue", + "Dataset", + "DatasetItem", + "DatasetRun", + "DatasetRunItem", + "DatasetRunWithItems", + "DatasetStatus", + "Error", + "MapValue", + "MethodNotAllowedError", + "Model", + "ModelPrice", + "ModelUsageUnit", + "NotFoundError", + "NumericScore", + "NumericScoreV1", + "Observation", + "ObservationLevel", + "ObservationV2", + "ObservationsView", + "PricingTier", + "PricingTierCondition", + "PricingTierInput", + "PricingTierOperator", + "Score", + "ScoreConfig", + "ScoreConfigDataType", + "ScoreDataType", + "ScoreSource", + "ScoreV1", + "ScoreV1_Boolean", + "ScoreV1_Categorical", + "ScoreV1_Numeric", + "ScoreV1_Text", + "Score_Boolean", + "Score_Categorical", + "Score_Correction", + "Score_Numeric", + "Score_Text", + "Session", + "SessionWithTraces", + "TextScore", + "TextScoreV1", + "Trace", + "TraceWithDetails", + "TraceWithFullDetails", + "UnauthorizedError", + "Usage", +] diff --git a/langfuse/api/commons/errors/__init__.py b/langfuse/api/commons/errors/__init__.py new file mode 100644 index 000000000..c633139f0 --- /dev/null +++ b/langfuse/api/commons/errors/__init__.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .access_denied_error import AccessDeniedError + from .error import Error + from .method_not_allowed_error import MethodNotAllowedError + from .not_found_error import NotFoundError + from .unauthorized_error import UnauthorizedError +_dynamic_imports: typing.Dict[str, str] = { + "AccessDeniedError": ".access_denied_error", + "Error": ".error", + "MethodNotAllowedError": ".method_not_allowed_error", + "NotFoundError": ".not_found_error", + "UnauthorizedError": ".unauthorized_error", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AccessDeniedError", + "Error", + "MethodNotAllowedError", + "NotFoundError", + "UnauthorizedError", +] diff --git a/langfuse/api/commons/errors/access_denied_error.py b/langfuse/api/commons/errors/access_denied_error.py new file mode 100644 index 000000000..156403fb7 --- /dev/null +++ b/langfuse/api/commons/errors/access_denied_error.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class AccessDeniedError(ApiError): + def __init__( + self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None + ): + super().__init__(status_code=403, headers=headers, body=body) diff --git a/langfuse/api/commons/errors/error.py b/langfuse/api/commons/errors/error.py new file mode 100644 index 000000000..5a8bd9639 --- /dev/null +++ b/langfuse/api/commons/errors/error.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class Error(ApiError): + def __init__( + self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None + ): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/langfuse/api/commons/errors/method_not_allowed_error.py b/langfuse/api/commons/errors/method_not_allowed_error.py new file mode 100644 index 000000000..436dd29dd --- /dev/null +++ b/langfuse/api/commons/errors/method_not_allowed_error.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class MethodNotAllowedError(ApiError): + def __init__( + self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None + ): + super().__init__(status_code=405, headers=headers, body=body) diff --git a/langfuse/api/commons/errors/not_found_error.py b/langfuse/api/commons/errors/not_found_error.py new file mode 100644 index 000000000..66b5bfc55 --- /dev/null +++ b/langfuse/api/commons/errors/not_found_error.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class NotFoundError(ApiError): + def __init__( + self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None + ): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/langfuse/api/commons/errors/unauthorized_error.py b/langfuse/api/commons/errors/unauthorized_error.py new file mode 100644 index 000000000..e71a01c5d --- /dev/null +++ b/langfuse/api/commons/errors/unauthorized_error.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class UnauthorizedError(ApiError): + def __init__( + self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None + ): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/langfuse/api/commons/types/__init__.py b/langfuse/api/commons/types/__init__.py new file mode 100644 index 000000000..5ce0a58cd --- /dev/null +++ b/langfuse/api/commons/types/__init__.py @@ -0,0 +1,207 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .base_score import BaseScore + from .base_score_v1 import BaseScoreV1 + from .boolean_score import BooleanScore + from .boolean_score_v1 import BooleanScoreV1 + from .categorical_score import CategoricalScore + from .categorical_score_v1 import CategoricalScoreV1 + from .comment import Comment + from .comment_object_type import CommentObjectType + from .config_category import ConfigCategory + from .correction_score import CorrectionScore + from .create_score_value import CreateScoreValue + from .dataset import Dataset + from .dataset_item import DatasetItem + from .dataset_run import DatasetRun + from .dataset_run_item import DatasetRunItem + from .dataset_run_with_items import DatasetRunWithItems + from .dataset_status import DatasetStatus + from .map_value import MapValue + from .model import Model + from .model_price import ModelPrice + from .model_usage_unit import ModelUsageUnit + from .numeric_score import NumericScore + from .numeric_score_v1 import NumericScoreV1 + from .observation import Observation + from .observation_level import ObservationLevel + from .observation_v2 import ObservationV2 + from .observations_view import ObservationsView + from .pricing_tier import PricingTier + from .pricing_tier_condition import PricingTierCondition + from .pricing_tier_input import PricingTierInput + from .pricing_tier_operator import PricingTierOperator + from .score import ( + Score, + Score_Boolean, + Score_Categorical, + Score_Correction, + Score_Numeric, + Score_Text, + ) + from .score_config import ScoreConfig + from .score_config_data_type import ScoreConfigDataType + from .score_data_type import ScoreDataType + from .score_source import ScoreSource + from .score_v1 import ( + ScoreV1, + ScoreV1_Boolean, + ScoreV1_Categorical, + ScoreV1_Numeric, + ScoreV1_Text, + ) + from .session import Session + from .session_with_traces import SessionWithTraces + from .text_score import TextScore + from .text_score_v1 import TextScoreV1 + from .trace import Trace + from .trace_with_details import TraceWithDetails + from .trace_with_full_details import TraceWithFullDetails + from .usage import Usage +_dynamic_imports: typing.Dict[str, str] = { + "BaseScore": ".base_score", + "BaseScoreV1": ".base_score_v1", + "BooleanScore": ".boolean_score", + "BooleanScoreV1": ".boolean_score_v1", + "CategoricalScore": ".categorical_score", + "CategoricalScoreV1": ".categorical_score_v1", + "Comment": ".comment", + "CommentObjectType": ".comment_object_type", + "ConfigCategory": ".config_category", + "CorrectionScore": ".correction_score", + "CreateScoreValue": ".create_score_value", + "Dataset": ".dataset", + "DatasetItem": ".dataset_item", + "DatasetRun": ".dataset_run", + "DatasetRunItem": ".dataset_run_item", + "DatasetRunWithItems": ".dataset_run_with_items", + "DatasetStatus": ".dataset_status", + "MapValue": ".map_value", + "Model": ".model", + "ModelPrice": ".model_price", + "ModelUsageUnit": ".model_usage_unit", + "NumericScore": ".numeric_score", + "NumericScoreV1": ".numeric_score_v1", + "Observation": ".observation", + "ObservationLevel": ".observation_level", + "ObservationV2": ".observation_v2", + "ObservationsView": ".observations_view", + "PricingTier": ".pricing_tier", + "PricingTierCondition": ".pricing_tier_condition", + "PricingTierInput": ".pricing_tier_input", + "PricingTierOperator": ".pricing_tier_operator", + "Score": ".score", + "ScoreConfig": ".score_config", + "ScoreConfigDataType": ".score_config_data_type", + "ScoreDataType": ".score_data_type", + "ScoreSource": ".score_source", + "ScoreV1": ".score_v1", + "ScoreV1_Boolean": ".score_v1", + "ScoreV1_Categorical": ".score_v1", + "ScoreV1_Numeric": ".score_v1", + "ScoreV1_Text": ".score_v1", + "Score_Boolean": ".score", + "Score_Categorical": ".score", + "Score_Correction": ".score", + "Score_Numeric": ".score", + "Score_Text": ".score", + "Session": ".session", + "SessionWithTraces": ".session_with_traces", + "TextScore": ".text_score", + "TextScoreV1": ".text_score_v1", + "Trace": ".trace", + "TraceWithDetails": ".trace_with_details", + "TraceWithFullDetails": ".trace_with_full_details", + "Usage": ".usage", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BaseScore", + "BaseScoreV1", + "BooleanScore", + "BooleanScoreV1", + "CategoricalScore", + "CategoricalScoreV1", + "Comment", + "CommentObjectType", + "ConfigCategory", + "CorrectionScore", + "CreateScoreValue", + "Dataset", + "DatasetItem", + "DatasetRun", + "DatasetRunItem", + "DatasetRunWithItems", + "DatasetStatus", + "MapValue", + "Model", + "ModelPrice", + "ModelUsageUnit", + "NumericScore", + "NumericScoreV1", + "Observation", + "ObservationLevel", + "ObservationV2", + "ObservationsView", + "PricingTier", + "PricingTierCondition", + "PricingTierInput", + "PricingTierOperator", + "Score", + "ScoreConfig", + "ScoreConfigDataType", + "ScoreDataType", + "ScoreSource", + "ScoreV1", + "ScoreV1_Boolean", + "ScoreV1_Categorical", + "ScoreV1_Numeric", + "ScoreV1_Text", + "Score_Boolean", + "Score_Categorical", + "Score_Correction", + "Score_Numeric", + "Score_Text", + "Session", + "SessionWithTraces", + "TextScore", + "TextScoreV1", + "Trace", + "TraceWithDetails", + "TraceWithFullDetails", + "Usage", +] diff --git a/langfuse/api/commons/types/base_score.py b/langfuse/api/commons/types/base_score.py new file mode 100644 index 000000000..44e09033c --- /dev/null +++ b/langfuse/api/commons/types/base_score.py @@ -0,0 +1,90 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .score_source import ScoreSource + + +class BaseScore(UniversalBaseModel): + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = pydantic.Field(default=None) + """ + The trace ID associated with the score + """ + + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = pydantic.Field(default=None) + """ + The session ID associated with the score + """ + + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = pydantic.Field(default=None) + """ + The observation ID associated with the score + """ + + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = pydantic.Field(default=None) + """ + The dataset run ID associated with the score + """ + + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = pydantic.Field(default=None) + """ + The user ID of the author + """ + + comment: typing.Optional[str] = pydantic.Field(default=None) + """ + Comment on the score + """ + + metadata: typing.Any = pydantic.Field() + """ + Metadata associated with the score + """ + + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = pydantic.Field(default=None) + """ + Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range + """ + + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = pydantic.Field(default=None) + """ + The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + """ + + environment: str = pydantic.Field() + """ + The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/base_score_v1.py b/langfuse/api/commons/types/base_score_v1.py new file mode 100644 index 000000000..881b10a3b --- /dev/null +++ b/langfuse/api/commons/types/base_score_v1.py @@ -0,0 +1,70 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .score_source import ScoreSource + + +class BaseScoreV1(UniversalBaseModel): + id: str + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + name: str + source: ScoreSource + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = pydantic.Field(default=None) + """ + The observation ID associated with the score + """ + + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = pydantic.Field(default=None) + """ + The user ID of the author + """ + + comment: typing.Optional[str] = pydantic.Field(default=None) + """ + Comment on the score + """ + + metadata: typing.Any = pydantic.Field() + """ + Metadata associated with the score + """ + + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = pydantic.Field(default=None) + """ + Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range + """ + + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = pydantic.Field(default=None) + """ + The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + """ + + environment: str = pydantic.Field() + """ + The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/boolean_score.py b/langfuse/api/commons/types/boolean_score.py new file mode 100644 index 000000000..2f65cf338 --- /dev/null +++ b/langfuse/api/commons/types/boolean_score.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .base_score import BaseScore + + +class BooleanScore(BaseScore): + value: float = pydantic.Field() + """ + The numeric value of the score. Equals 1 for "True" and 0 for "False" + """ + + string_value: typing_extensions.Annotated[ + str, FieldMetadata(alias="stringValue") + ] = pydantic.Field() + """ + The string representation of the score value. Is inferred from the numeric value and equals "True" or "False" + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/boolean_score_v1.py b/langfuse/api/commons/types/boolean_score_v1.py new file mode 100644 index 000000000..cf5425255 --- /dev/null +++ b/langfuse/api/commons/types/boolean_score_v1.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .base_score_v1 import BaseScoreV1 + + +class BooleanScoreV1(BaseScoreV1): + value: float = pydantic.Field() + """ + The numeric value of the score. Equals 1 for "True" and 0 for "False" + """ + + string_value: typing_extensions.Annotated[ + str, FieldMetadata(alias="stringValue") + ] = pydantic.Field() + """ + The string representation of the score value. Is inferred from the numeric value and equals "True" or "False" + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/categorical_score.py b/langfuse/api/commons/types/categorical_score.py new file mode 100644 index 000000000..a12ac58c3 --- /dev/null +++ b/langfuse/api/commons/types/categorical_score.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .base_score import BaseScore + + +class CategoricalScore(BaseScore): + value: float = pydantic.Field() + """ + Represents the numeric category mapping of the stringValue. If no config is linked, defaults to 0. + """ + + string_value: typing_extensions.Annotated[ + str, FieldMetadata(alias="stringValue") + ] = pydantic.Field() + """ + The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/categorical_score_v1.py b/langfuse/api/commons/types/categorical_score_v1.py new file mode 100644 index 000000000..8f98af1a8 --- /dev/null +++ b/langfuse/api/commons/types/categorical_score_v1.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .base_score_v1 import BaseScoreV1 + + +class CategoricalScoreV1(BaseScoreV1): + value: float = pydantic.Field() + """ + Represents the numeric category mapping of the stringValue. If no config is linked, defaults to 0. + """ + + string_value: typing_extensions.Annotated[ + str, FieldMetadata(alias="stringValue") + ] = pydantic.Field() + """ + The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/comment.py b/langfuse/api/commons/types/comment.py new file mode 100644 index 000000000..31daeeed8 --- /dev/null +++ b/langfuse/api/commons/types/comment.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .comment_object_type import CommentObjectType + + +class Comment(UniversalBaseModel): + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + object_type: typing_extensions.Annotated[ + CommentObjectType, FieldMetadata(alias="objectType") + ] + object_id: typing_extensions.Annotated[str, FieldMetadata(alias="objectId")] + content: str + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = pydantic.Field(default=None) + """ + The user ID of the comment author + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/resources/commons/types/comment_object_type.py b/langfuse/api/commons/types/comment_object_type.py similarity index 92% rename from langfuse/api/resources/commons/types/comment_object_type.py rename to langfuse/api/commons/types/comment_object_type.py index 9c6c134c6..fbd0d4e49 100644 --- a/langfuse/api/resources/commons/types/comment_object_type.py +++ b/langfuse/api/commons/types/comment_object_type.py @@ -1,12 +1,13 @@ # This file was auto-generated by Fern from our API Definition. -import enum import typing +from ...core import enum + T_Result = typing.TypeVar("T_Result") -class CommentObjectType(str, enum.Enum): +class CommentObjectType(enum.StrEnum): TRACE = "TRACE" OBSERVATION = "OBSERVATION" SESSION = "SESSION" diff --git a/langfuse/api/commons/types/config_category.py b/langfuse/api/commons/types/config_category.py new file mode 100644 index 000000000..4ca546e35 --- /dev/null +++ b/langfuse/api/commons/types/config_category.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ConfigCategory(UniversalBaseModel): + value: float + label: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/correction_score.py b/langfuse/api/commons/types/correction_score.py new file mode 100644 index 000000000..9b37071f4 --- /dev/null +++ b/langfuse/api/commons/types/correction_score.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .base_score import BaseScore + + +class CorrectionScore(BaseScore): + value: float = pydantic.Field() + """ + The numeric value of the score. Always 0 for correction scores. + """ + + string_value: typing_extensions.Annotated[ + str, FieldMetadata(alias="stringValue") + ] = pydantic.Field() + """ + The string representation of the correction content + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/resources/commons/types/create_score_value.py b/langfuse/api/commons/types/create_score_value.py similarity index 100% rename from langfuse/api/resources/commons/types/create_score_value.py rename to langfuse/api/commons/types/create_score_value.py diff --git a/langfuse/api/commons/types/dataset.py b/langfuse/api/commons/types/dataset.py new file mode 100644 index 000000000..d312b291a --- /dev/null +++ b/langfuse/api/commons/types/dataset.py @@ -0,0 +1,49 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class Dataset(UniversalBaseModel): + id: str + name: str + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Description of the dataset + """ + + metadata: typing.Any = pydantic.Field() + """ + Metadata associated with the dataset + """ + + input_schema: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="inputSchema") + ] = pydantic.Field(default=None) + """ + JSON Schema for validating dataset item inputs + """ + + expected_output_schema: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="expectedOutputSchema") + ] = pydantic.Field(default=None) + """ + JSON Schema for validating dataset item expected outputs + """ + + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/dataset_item.py b/langfuse/api/commons/types/dataset_item.py new file mode 100644 index 000000000..54a13d81a --- /dev/null +++ b/langfuse/api/commons/types/dataset_item.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .dataset_status import DatasetStatus + + +class DatasetItem(UniversalBaseModel): + id: str + status: DatasetStatus + input: typing.Any = pydantic.Field() + """ + Input data for the dataset item + """ + + expected_output: typing_extensions.Annotated[ + typing.Any, FieldMetadata(alias="expectedOutput") + ] = pydantic.Field() + """ + Expected output for the dataset item + """ + + metadata: typing.Any = pydantic.Field() + """ + Metadata associated with the dataset item + """ + + source_trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sourceTraceId") + ] = pydantic.Field(default=None) + """ + The trace ID that sourced this dataset item + """ + + source_observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sourceObservationId") + ] = pydantic.Field(default=None) + """ + The observation ID that sourced this dataset item + """ + + dataset_id: typing_extensions.Annotated[str, FieldMetadata(alias="datasetId")] + dataset_name: typing_extensions.Annotated[str, FieldMetadata(alias="datasetName")] + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/dataset_run.py b/langfuse/api/commons/types/dataset_run.py new file mode 100644 index 000000000..0b5d8566c --- /dev/null +++ b/langfuse/api/commons/types/dataset_run.py @@ -0,0 +1,63 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class DatasetRun(UniversalBaseModel): + id: str = pydantic.Field() + """ + Unique identifier of the dataset run + """ + + name: str = pydantic.Field() + """ + Name of the dataset run + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Description of the run + """ + + metadata: typing.Any = pydantic.Field() + """ + Metadata of the dataset run + """ + + dataset_id: typing_extensions.Annotated[str, FieldMetadata(alias="datasetId")] = ( + pydantic.Field() + ) + """ + Id of the associated dataset + """ + + dataset_name: typing_extensions.Annotated[ + str, FieldMetadata(alias="datasetName") + ] = pydantic.Field() + """ + Name of the associated dataset + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] = pydantic.Field() + """ + The date and time when the dataset run was created + """ + + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] = pydantic.Field() + """ + The date and time when the dataset run was last updated + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/dataset_run_item.py b/langfuse/api/commons/types/dataset_run_item.py new file mode 100644 index 000000000..1ee364ffa --- /dev/null +++ b/langfuse/api/commons/types/dataset_run_item.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class DatasetRunItem(UniversalBaseModel): + id: str + dataset_run_id: typing_extensions.Annotated[ + str, FieldMetadata(alias="datasetRunId") + ] + dataset_run_name: typing_extensions.Annotated[ + str, FieldMetadata(alias="datasetRunName") + ] + dataset_item_id: typing_extensions.Annotated[ + str, FieldMetadata(alias="datasetItemId") + ] + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = pydantic.Field(default=None) + """ + The observation ID associated with this run item + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/dataset_run_with_items.py b/langfuse/api/commons/types/dataset_run_with_items.py new file mode 100644 index 000000000..b5995dd30 --- /dev/null +++ b/langfuse/api/commons/types/dataset_run_with_items.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .dataset_run import DatasetRun +from .dataset_run_item import DatasetRunItem + + +class DatasetRunWithItems(DatasetRun): + dataset_run_items: typing_extensions.Annotated[ + typing.List[DatasetRunItem], FieldMetadata(alias="datasetRunItems") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/resources/commons/types/dataset_status.py b/langfuse/api/commons/types/dataset_status.py similarity index 88% rename from langfuse/api/resources/commons/types/dataset_status.py rename to langfuse/api/commons/types/dataset_status.py index 09eac62fe..f8e681aeb 100644 --- a/langfuse/api/resources/commons/types/dataset_status.py +++ b/langfuse/api/commons/types/dataset_status.py @@ -1,12 +1,13 @@ # This file was auto-generated by Fern from our API Definition. -import enum import typing +from ...core import enum + T_Result = typing.TypeVar("T_Result") -class DatasetStatus(str, enum.Enum): +class DatasetStatus(enum.StrEnum): ACTIVE = "ACTIVE" ARCHIVED = "ARCHIVED" diff --git a/langfuse/api/resources/commons/types/map_value.py b/langfuse/api/commons/types/map_value.py similarity index 100% rename from langfuse/api/resources/commons/types/map_value.py rename to langfuse/api/commons/types/map_value.py diff --git a/langfuse/api/commons/types/model.py b/langfuse/api/commons/types/model.py new file mode 100644 index 000000000..e09313e8a --- /dev/null +++ b/langfuse/api/commons/types/model.py @@ -0,0 +1,125 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .model_price import ModelPrice +from .model_usage_unit import ModelUsageUnit +from .pricing_tier import PricingTier + + +class Model(UniversalBaseModel): + """ + Model definition used for transforming usage into USD cost and/or tokenization. + + Models can have either simple flat pricing or tiered pricing: + - Flat pricing: Single price per usage type (legacy, but still supported) + - Tiered pricing: Multiple pricing tiers with conditional matching based on usage patterns + + The pricing tiers approach is recommended for models with usage-based pricing variations. + When using tiered pricing, the flat price fields (inputPrice, outputPrice, prices) are populated + from the default tier for backward compatibility. + """ + + id: str + model_name: typing_extensions.Annotated[str, FieldMetadata(alias="modelName")] = ( + pydantic.Field() + ) + """ + Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime200K tokens), Priority 2 for "medium usage" (>100K tokens), Priority 0 for default + - Without proper ordering, a less specific condition might match before a more specific one + + Every model must have exactly one default tier to ensure cost calculation always succeeds. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the pricing tier + """ + + name: str = pydantic.Field() + """ + Name of the pricing tier for display and identification purposes. + + Examples: "Standard", "High Volume Tier", "Large Context", "Extended Context Tier" + """ + + is_default: typing_extensions.Annotated[bool, FieldMetadata(alias="isDefault")] = ( + pydantic.Field() + ) + """ + Whether this is the default tier. Every model must have exactly one default tier with priority 0 and no conditions. + + The default tier serves as a fallback when no conditional tiers match, ensuring cost calculation always succeeds. + It typically represents the base pricing for standard usage patterns. + """ + + priority: int = pydantic.Field() + """ + Priority for tier matching evaluation. Lower numbers = higher priority (evaluated first). + + The default tier must always have priority 0. Conditional tiers should have priority 1, 2, 3, etc. + + Example ordering: + - Priority 0: Default tier (no conditions, always matches as fallback) + - Priority 1: High usage tier (e.g., >200K tokens) + - Priority 2: Medium usage tier (e.g., >100K tokens) + + This ensures more specific conditions are checked before general ones. + """ + + conditions: typing.List[PricingTierCondition] = pydantic.Field() + """ + Array of conditions that must ALL be met for this tier to match (AND logic). + + The default tier must have an empty conditions array. Conditional tiers should have one or more conditions + that define when this tier's pricing applies. + + Multiple conditions enable complex matching scenarios (e.g., "high input tokens AND low output tokens"). + """ + + prices: typing.Dict[str, float] = pydantic.Field() + """ + Prices (USD) by usage type for this tier. + + Common usage types: "input", "output", "total", "request", "image" + Prices are specified in USD per unit (e.g., per token, per request, per second). + + Example: {"input": 0.000003, "output": 0.000015} means $3 per million input tokens and $15 per million output tokens. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/pricing_tier_condition.py b/langfuse/api/commons/types/pricing_tier_condition.py new file mode 100644 index 000000000..9ebbba0af --- /dev/null +++ b/langfuse/api/commons/types/pricing_tier_condition.py @@ -0,0 +1,68 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .pricing_tier_operator import PricingTierOperator + + +class PricingTierCondition(UniversalBaseModel): + """ + Condition for matching a pricing tier based on usage details. Used to implement tiered pricing models where costs vary based on usage thresholds. + + How it works: + 1. The regex pattern matches against usage detail keys (e.g., "input_tokens", "input_cached") + 2. Values of all matching keys are summed together + 3. The sum is compared against the threshold value using the specified operator + 4. All conditions in a tier must be met (AND logic) for the tier to match + + Common use cases: + - Threshold-based pricing: Match when accumulated usage exceeds a certain amount + - Usage-type-specific pricing: Different rates for cached vs non-cached tokens, or input vs output + - Volume-based pricing: Different rates based on total request or token count + """ + + usage_detail_pattern: typing_extensions.Annotated[ + str, FieldMetadata(alias="usageDetailPattern") + ] = pydantic.Field() + """ + Regex pattern to match against usage detail keys. All matching keys' values are summed for threshold comparison. + + Examples: + - "^input" matches "input", "input_tokens", "input_cached", etc. + - "^(input|prompt)" matches both "input_tokens" and "prompt_tokens" + - "_cache$" matches "input_cache", "output_cache", etc. + + The pattern is case-insensitive by default. If no keys match, the sum is treated as zero. + """ + + operator: PricingTierOperator = pydantic.Field() + """ + Comparison operator to apply between the summed value and the threshold. + + - gt: greater than (sum > threshold) + - gte: greater than or equal (sum >= threshold) + - lt: less than (sum < threshold) + - lte: less than or equal (sum <= threshold) + - eq: equal (sum == threshold) + - neq: not equal (sum != threshold) + """ + + value: float = pydantic.Field() + """ + Threshold value for comparison. For token-based pricing, this is typically the token count threshold (e.g., 200000 for a 200K token threshold). + """ + + case_sensitive: typing_extensions.Annotated[ + bool, FieldMetadata(alias="caseSensitive") + ] = pydantic.Field() + """ + Whether the regex pattern matching is case-sensitive. Default is false (case-insensitive matching). + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/pricing_tier_input.py b/langfuse/api/commons/types/pricing_tier_input.py new file mode 100644 index 000000000..c25d7f716 --- /dev/null +++ b/langfuse/api/commons/types/pricing_tier_input.py @@ -0,0 +1,76 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .pricing_tier_condition import PricingTierCondition + + +class PricingTierInput(UniversalBaseModel): + """ + Input schema for creating a pricing tier. The tier ID will be automatically generated server-side. + + When creating a model with pricing tiers: + - Exactly one tier must have isDefault=true (the fallback tier) + - The default tier must have priority=0 and conditions=[] + - All tier names and priorities must be unique within the model + - Each tier must define at least one price + + See PricingTier for detailed information about how tiers work and why they're useful. + """ + + name: str = pydantic.Field() + """ + Name of the pricing tier for display and identification purposes. + + Must be unique within the model. Common patterns: "Standard", "High Volume Tier", "Extended Context" + """ + + is_default: typing_extensions.Annotated[bool, FieldMetadata(alias="isDefault")] = ( + pydantic.Field() + ) + """ + Whether this is the default tier. Exactly one tier per model must be marked as default. + + Requirements for default tier: + - Must have isDefault=true + - Must have priority=0 + - Must have empty conditions array (conditions=[]) + + The default tier acts as a fallback when no conditional tiers match. + """ + + priority: int = pydantic.Field() + """ + Priority for tier matching evaluation. Lower numbers = higher priority (evaluated first). + + Must be unique within the model. The default tier must have priority=0. + Conditional tiers should use priority 1, 2, 3, etc. based on their specificity. + """ + + conditions: typing.List[PricingTierCondition] = pydantic.Field() + """ + Array of conditions that must ALL be met for this tier to match (AND logic). + + The default tier must have an empty array (conditions=[]). + Conditional tiers should define one or more conditions that specify when this tier's pricing applies. + + Each condition specifies a regex pattern, operator, and threshold value for matching against usage details. + """ + + prices: typing.Dict[str, float] = pydantic.Field() + """ + Prices (USD) by usage type for this tier. At least one price must be defined. + + Common usage types: "input", "output", "total", "request", "image" + Prices are in USD per unit (e.g., per token). + + Example: {"input": 0.000003, "output": 0.000015} represents $3 per million input tokens and $15 per million output tokens. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/pricing_tier_operator.py b/langfuse/api/commons/types/pricing_tier_operator.py new file mode 100644 index 000000000..6d40799ec --- /dev/null +++ b/langfuse/api/commons/types/pricing_tier_operator.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class PricingTierOperator(enum.StrEnum): + """ + Comparison operators for pricing tier conditions + """ + + GT = "gt" + GTE = "gte" + LT = "lt" + LTE = "lte" + EQ = "eq" + NEQ = "neq" + + def visit( + self, + gt: typing.Callable[[], T_Result], + gte: typing.Callable[[], T_Result], + lt: typing.Callable[[], T_Result], + lte: typing.Callable[[], T_Result], + eq: typing.Callable[[], T_Result], + neq: typing.Callable[[], T_Result], + ) -> T_Result: + if self is PricingTierOperator.GT: + return gt() + if self is PricingTierOperator.GTE: + return gte() + if self is PricingTierOperator.LT: + return lt() + if self is PricingTierOperator.LTE: + return lte() + if self is PricingTierOperator.EQ: + return eq() + if self is PricingTierOperator.NEQ: + return neq() diff --git a/langfuse/api/commons/types/score.py b/langfuse/api/commons/types/score.py new file mode 100644 index 000000000..33c901c5f --- /dev/null +++ b/langfuse/api/commons/types/score.py @@ -0,0 +1,248 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .score_source import ScoreSource + + +class Score_Numeric(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["NUMERIC"], FieldMetadata(alias="dataType") + ] = "NUMERIC" + value: float + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class Score_Categorical(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["CATEGORICAL"], FieldMetadata(alias="dataType") + ] = "CATEGORICAL" + value: float + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class Score_Boolean(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["BOOLEAN"], FieldMetadata(alias="dataType") + ] = "BOOLEAN" + value: float + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class Score_Correction(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["CORRECTION"], FieldMetadata(alias="dataType") + ] = "CORRECTION" + value: float + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class Score_Text(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["TEXT"], FieldMetadata(alias="dataType") + ] = "TEXT" + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +Score = typing_extensions.Annotated[ + typing.Union[ + Score_Numeric, Score_Categorical, Score_Boolean, Score_Correction, Score_Text + ], + pydantic.Field(discriminator="data_type"), +] diff --git a/langfuse/api/commons/types/score_config.py b/langfuse/api/commons/types/score_config.py new file mode 100644 index 000000000..36e45e168 --- /dev/null +++ b/langfuse/api/commons/types/score_config.py @@ -0,0 +1,66 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .config_category import ConfigCategory +from .score_config_data_type import ScoreConfigDataType + + +class ScoreConfig(UniversalBaseModel): + """ + Configuration for a score + """ + + id: str + name: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + data_type: typing_extensions.Annotated[ + ScoreConfigDataType, FieldMetadata(alias="dataType") + ] + is_archived: typing_extensions.Annotated[ + bool, FieldMetadata(alias="isArchived") + ] = pydantic.Field() + """ + Whether the score config is archived. Defaults to false + """ + + min_value: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="minValue") + ] = pydantic.Field(default=None) + """ + Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞ + """ + + max_value: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="maxValue") + ] = pydantic.Field(default=None) + """ + Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞ + """ + + categories: typing.Optional[typing.List[ConfigCategory]] = pydantic.Field( + default=None + ) + """ + Configures custom categories for categorical scores + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Description of the score config + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/score_config_data_type.py b/langfuse/api/commons/types/score_config_data_type.py new file mode 100644 index 000000000..683b46a0f --- /dev/null +++ b/langfuse/api/commons/types/score_config_data_type.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ScoreConfigDataType(enum.StrEnum): + NUMERIC = "NUMERIC" + BOOLEAN = "BOOLEAN" + CATEGORICAL = "CATEGORICAL" + TEXT = "TEXT" + + def visit( + self, + numeric: typing.Callable[[], T_Result], + boolean: typing.Callable[[], T_Result], + categorical: typing.Callable[[], T_Result], + text: typing.Callable[[], T_Result], + ) -> T_Result: + if self is ScoreConfigDataType.NUMERIC: + return numeric() + if self is ScoreConfigDataType.BOOLEAN: + return boolean() + if self is ScoreConfigDataType.CATEGORICAL: + return categorical() + if self is ScoreConfigDataType.TEXT: + return text() diff --git a/langfuse/api/resources/commons/types/score_data_type.py b/langfuse/api/commons/types/score_data_type.py similarity index 64% rename from langfuse/api/resources/commons/types/score_data_type.py rename to langfuse/api/commons/types/score_data_type.py index c2eed12cd..18301b51f 100644 --- a/langfuse/api/resources/commons/types/score_data_type.py +++ b/langfuse/api/commons/types/score_data_type.py @@ -1,21 +1,26 @@ # This file was auto-generated by Fern from our API Definition. -import enum import typing +from ...core import enum + T_Result = typing.TypeVar("T_Result") -class ScoreDataType(str, enum.Enum): +class ScoreDataType(enum.StrEnum): NUMERIC = "NUMERIC" BOOLEAN = "BOOLEAN" CATEGORICAL = "CATEGORICAL" + CORRECTION = "CORRECTION" + TEXT = "TEXT" def visit( self, numeric: typing.Callable[[], T_Result], boolean: typing.Callable[[], T_Result], categorical: typing.Callable[[], T_Result], + correction: typing.Callable[[], T_Result], + text: typing.Callable[[], T_Result], ) -> T_Result: if self is ScoreDataType.NUMERIC: return numeric() @@ -23,3 +28,7 @@ def visit( return boolean() if self is ScoreDataType.CATEGORICAL: return categorical() + if self is ScoreDataType.CORRECTION: + return correction() + if self is ScoreDataType.TEXT: + return text() diff --git a/langfuse/api/resources/commons/types/score_source.py b/langfuse/api/commons/types/score_source.py similarity index 90% rename from langfuse/api/resources/commons/types/score_source.py rename to langfuse/api/commons/types/score_source.py index 699f078b7..ee1398a9a 100644 --- a/langfuse/api/resources/commons/types/score_source.py +++ b/langfuse/api/commons/types/score_source.py @@ -1,12 +1,13 @@ # This file was auto-generated by Fern from our API Definition. -import enum import typing +from ...core import enum + T_Result = typing.TypeVar("T_Result") -class ScoreSource(str, enum.Enum): +class ScoreSource(enum.StrEnum): ANNOTATION = "ANNOTATION" API = "API" EVAL = "EVAL" diff --git a/langfuse/api/commons/types/score_v1.py b/langfuse/api/commons/types/score_v1.py new file mode 100644 index 000000000..e17e61b89 --- /dev/null +++ b/langfuse/api/commons/types/score_v1.py @@ -0,0 +1,168 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .score_source import ScoreSource + + +class ScoreV1_Numeric(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["NUMERIC"], FieldMetadata(alias="dataType") + ] = "NUMERIC" + value: float + id: str + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + name: str + source: ScoreSource + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreV1_Categorical(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["CATEGORICAL"], FieldMetadata(alias="dataType") + ] = "CATEGORICAL" + value: float + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + name: str + source: ScoreSource + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreV1_Boolean(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["BOOLEAN"], FieldMetadata(alias="dataType") + ] = "BOOLEAN" + value: float + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + name: str + source: ScoreSource + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreV1_Text(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["TEXT"], FieldMetadata(alias="dataType") + ] = "TEXT" + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] + name: str + source: ScoreSource + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +ScoreV1 = typing_extensions.Annotated[ + typing.Union[ScoreV1_Numeric, ScoreV1_Categorical, ScoreV1_Boolean, ScoreV1_Text], + pydantic.Field(discriminator="data_type"), +] diff --git a/langfuse/api/commons/types/session.py b/langfuse/api/commons/types/session.py new file mode 100644 index 000000000..c254b27cd --- /dev/null +++ b/langfuse/api/commons/types/session.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class Session(UniversalBaseModel): + id: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + environment: str = pydantic.Field() + """ + The environment from which this session originated. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/session_with_traces.py b/langfuse/api/commons/types/session_with_traces.py new file mode 100644 index 000000000..d04eef54e --- /dev/null +++ b/langfuse/api/commons/types/session_with_traces.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .session import Session +from .trace import Trace + + +class SessionWithTraces(Session): + traces: typing.List[Trace] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/text_score.py b/langfuse/api/commons/types/text_score.py new file mode 100644 index 000000000..438e30f47 --- /dev/null +++ b/langfuse/api/commons/types/text_score.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .base_score import BaseScore + + +class TextScore(BaseScore): + string_value: typing_extensions.Annotated[ + str, FieldMetadata(alias="stringValue") + ] = pydantic.Field() + """ + The text content of the score (1-500 characters) + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/text_score_v1.py b/langfuse/api/commons/types/text_score_v1.py new file mode 100644 index 000000000..937ca281e --- /dev/null +++ b/langfuse/api/commons/types/text_score_v1.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .base_score_v1 import BaseScoreV1 + + +class TextScoreV1(BaseScoreV1): + string_value: typing_extensions.Annotated[ + str, FieldMetadata(alias="stringValue") + ] = pydantic.Field() + """ + The text content of the score (1-500 characters) + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/trace.py b/langfuse/api/commons/types/trace.py new file mode 100644 index 000000000..6eafa9fcd --- /dev/null +++ b/langfuse/api/commons/types/trace.py @@ -0,0 +1,84 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class Trace(UniversalBaseModel): + id: str = pydantic.Field() + """ + The unique identifier of a trace + """ + + timestamp: dt.datetime = pydantic.Field() + """ + The timestamp when the trace was created + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + The name of the trace + """ + + input: typing.Optional[typing.Any] = pydantic.Field(default=None) + """ + The input data of the trace. Can be any JSON. + """ + + output: typing.Optional[typing.Any] = pydantic.Field(default=None) + """ + The output data of the trace. Can be any JSON. + """ + + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = pydantic.Field(default=None) + """ + The session identifier associated with the trace + """ + + release: typing.Optional[str] = pydantic.Field(default=None) + """ + The release version of the application when the trace was created + """ + + version: typing.Optional[str] = pydantic.Field(default=None) + """ + The version of the trace + """ + + user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="userId") + ] = pydantic.Field(default=None) + """ + The user identifier associated with the trace + """ + + metadata: typing.Optional[typing.Any] = pydantic.Field(default=None) + """ + The metadata associated with the trace. Can be any JSON. + """ + + tags: typing.List[str] = pydantic.Field() + """ + The tags associated with the trace. + """ + + public: bool = pydantic.Field() + """ + Public traces are accessible via url without login + """ + + environment: str = pydantic.Field() + """ + The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/trace_with_details.py b/langfuse/api/commons/types/trace_with_details.py new file mode 100644 index 000000000..958d76901 --- /dev/null +++ b/langfuse/api/commons/types/trace_with_details.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .trace import Trace + + +class TraceWithDetails(Trace): + html_path: typing_extensions.Annotated[str, FieldMetadata(alias="htmlPath")] = ( + pydantic.Field() + ) + """ + Path of trace in Langfuse UI + """ + + latency: typing.Optional[float] = pydantic.Field(default=None) + """ + Latency of trace in seconds + """ + + total_cost: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="totalCost") + ] = pydantic.Field(default=None) + """ + Cost of trace in USD + """ + + observations: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of observation ids + """ + + scores: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of score ids + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/trace_with_full_details.py b/langfuse/api/commons/types/trace_with_full_details.py new file mode 100644 index 000000000..f14757ce5 --- /dev/null +++ b/langfuse/api/commons/types/trace_with_full_details.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .observations_view import ObservationsView +from .score_v1 import ScoreV1 +from .trace import Trace + + +class TraceWithFullDetails(Trace): + html_path: typing_extensions.Annotated[str, FieldMetadata(alias="htmlPath")] = ( + pydantic.Field() + ) + """ + Path of trace in Langfuse UI + """ + + latency: typing.Optional[float] = pydantic.Field(default=None) + """ + Latency of trace in seconds + """ + + total_cost: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="totalCost") + ] = pydantic.Field(default=None) + """ + Cost of trace in USD + """ + + observations: typing.List[ObservationsView] = pydantic.Field() + """ + List of observations + """ + + scores: typing.List[ScoreV1] = pydantic.Field() + """ + List of scores + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/commons/types/usage.py b/langfuse/api/commons/types/usage.py new file mode 100644 index 000000000..b3c1fd048 --- /dev/null +++ b/langfuse/api/commons/types/usage.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class Usage(UniversalBaseModel): + """ + (Deprecated. Use usageDetails and costDetails instead.) Standard interface for usage and cost + """ + + input: int = pydantic.Field() + """ + Number of input units (e.g. tokens) + """ + + output: int = pydantic.Field() + """ + Number of output units (e.g. tokens) + """ + + total: int = pydantic.Field() + """ + Defaults to input+output if not set + """ + + unit: typing.Optional[str] = pydantic.Field(default=None) + """ + Unit of measurement + """ + + input_cost: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="inputCost") + ] = pydantic.Field(default=None) + """ + USD input cost + """ + + output_cost: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="outputCost") + ] = pydantic.Field(default=None) + """ + USD output cost + """ + + total_cost: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="totalCost") + ] = pydantic.Field(default=None) + """ + USD total cost, defaults to input+output + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/core/__init__.py b/langfuse/api/core/__init__.py index 58ad52ad2..91742cd87 100644 --- a/langfuse/api/core/__init__.py +++ b/langfuse/api/core/__init__.py @@ -1,30 +1,111 @@ # This file was auto-generated by Fern from our API Definition. -from .api_error import ApiError -from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper -from .datetime_utils import serialize_datetime -from .file import File, convert_file_dict_to_httpx_tuples -from .http_client import AsyncHttpClient, HttpClient -from .jsonable_encoder import jsonable_encoder -from .pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .query_encoder import encode_query -from .remove_none_from_dict import remove_none_from_dict -from .request_options import RequestOptions +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .api_error import ApiError + from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper + from .datetime_utils import serialize_datetime + from .file import File, convert_file_dict_to_httpx_tuples, with_content_type + from .http_client import AsyncHttpClient, HttpClient + from .http_response import AsyncHttpResponse, HttpResponse + from .jsonable_encoder import jsonable_encoder + from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, + ) + from .query_encoder import encode_query + from .remove_none_from_dict import remove_none_from_dict + from .request_options import RequestOptions + from .serialization import FieldMetadata, convert_and_respect_annotation_metadata +_dynamic_imports: typing.Dict[str, str] = { + "ApiError": ".api_error", + "AsyncClientWrapper": ".client_wrapper", + "AsyncHttpClient": ".http_client", + "AsyncHttpResponse": ".http_response", + "BaseClientWrapper": ".client_wrapper", + "FieldMetadata": ".serialization", + "File": ".file", + "HttpClient": ".http_client", + "HttpResponse": ".http_response", + "IS_PYDANTIC_V2": ".pydantic_utilities", + "RequestOptions": ".request_options", + "SyncClientWrapper": ".client_wrapper", + "UniversalBaseModel": ".pydantic_utilities", + "UniversalRootModel": ".pydantic_utilities", + "convert_and_respect_annotation_metadata": ".serialization", + "convert_file_dict_to_httpx_tuples": ".file", + "encode_query": ".query_encoder", + "jsonable_encoder": ".jsonable_encoder", + "parse_obj_as": ".pydantic_utilities", + "remove_none_from_dict": ".remove_none_from_dict", + "serialize_datetime": ".datetime_utils", + "universal_field_validator": ".pydantic_utilities", + "universal_root_validator": ".pydantic_utilities", + "update_forward_refs": ".pydantic_utilities", + "with_content_type": ".file", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + __all__ = [ "ApiError", "AsyncClientWrapper", "AsyncHttpClient", + "AsyncHttpResponse", "BaseClientWrapper", + "FieldMetadata", "File", "HttpClient", + "HttpResponse", + "IS_PYDANTIC_V2", "RequestOptions", "SyncClientWrapper", + "UniversalBaseModel", + "UniversalRootModel", + "convert_and_respect_annotation_metadata", "convert_file_dict_to_httpx_tuples", - "deep_union_pydantic_dicts", "encode_query", "jsonable_encoder", - "pydantic_v1", + "parse_obj_as", "remove_none_from_dict", "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", + "with_content_type", ] diff --git a/langfuse/api/core/api_error.py b/langfuse/api/core/api_error.py index da734b580..6f850a60c 100644 --- a/langfuse/api/core/api_error.py +++ b/langfuse/api/core/api_error.py @@ -1,17 +1,23 @@ # This file was auto-generated by Fern from our API Definition. -import typing +from typing import Any, Dict, Optional class ApiError(Exception): - status_code: typing.Optional[int] - body: typing.Any + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any def __init__( - self, *, status_code: typing.Optional[int] = None, body: typing.Any = None - ): + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers self.status_code = status_code self.body = body def __str__(self) -> str: - return f"status_code: {self.status_code}, body: {self.body}" + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/langfuse/api/core/client_wrapper.py b/langfuse/api/core/client_wrapper.py index 8a053f4a7..22bb6a70b 100644 --- a/langfuse/api/core/client_wrapper.py +++ b/langfuse/api/core/client_wrapper.py @@ -3,7 +3,6 @@ import typing import httpx - from .http_client import AsyncHttpClient, HttpClient @@ -16,6 +15,7 @@ def __init__( x_langfuse_public_key: typing.Optional[str] = None, username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, ): @@ -24,11 +24,15 @@ def __init__( self._x_langfuse_public_key = x_langfuse_public_key self._username = username self._password = password + self._headers = headers self._base_url = base_url self._timeout = timeout def get_headers(self) -> typing.Dict[str, str]: - headers: typing.Dict[str, str] = {"X-Fern-Language": "Python"} + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + **(self.get_custom_headers() or {}), + } username = self._get_username() password = self._get_password() if username is not None and password is not None: @@ -53,6 +57,9 @@ def _get_password(self) -> typing.Optional[str]: else: return self._password() + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + def get_base_url(self) -> str: return self._base_url @@ -69,6 +76,7 @@ def __init__( x_langfuse_public_key: typing.Optional[str] = None, username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, httpx_client: httpx.Client, @@ -79,14 +87,15 @@ def __init__( x_langfuse_public_key=x_langfuse_public_key, username=username, password=password, + headers=headers, base_url=base_url, timeout=timeout, ) self.httpx_client = HttpClient( httpx_client=httpx_client, - base_headers=self.get_headers(), - base_timeout=self.get_timeout(), - base_url=self.get_base_url(), + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, ) @@ -99,8 +108,10 @@ def __init__( x_langfuse_public_key: typing.Optional[str] = None, username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, httpx_client: httpx.AsyncClient, ): super().__init__( @@ -109,12 +120,22 @@ def __init__( x_langfuse_public_key=x_langfuse_public_key, username=username, password=password, + headers=headers, base_url=base_url, timeout=timeout, ) + self._async_token = async_token self.httpx_client = AsyncHttpClient( httpx_client=httpx_client, - base_headers=self.get_headers(), - base_timeout=self.get_timeout(), - base_url=self.get_base_url(), + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + async_base_headers=self.async_get_headers, ) + + async def async_get_headers(self) -> typing.Dict[str, str]: + headers = self.get_headers() + if self._async_token is not None: + token = await self._async_token() + headers["Authorization"] = f"Bearer {token}" + return headers diff --git a/langfuse/api/core/enum.py b/langfuse/api/core/enum.py new file mode 100644 index 000000000..a3d17a67b --- /dev/null +++ b/langfuse/api/core/enum.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +Provides a StrEnum base class that works across Python versions. + +For Python >= 3.11, this re-exports the standard library enum.StrEnum. +For older Python versions, this defines a compatible StrEnum using the +(str, Enum) mixin pattern so that generated SDKs can use a single base +class in all supported Python versions. +""" + +import enum +import sys + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + + class StrEnum(str, enum.Enum): + pass diff --git a/langfuse/api/core/file.py b/langfuse/api/core/file.py index 6e0f92bfc..3467175cb 100644 --- a/langfuse/api/core/file.py +++ b/langfuse/api/core/file.py @@ -1,30 +1,30 @@ # This file was auto-generated by Fern from our API Definition. -import typing +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast # File typing inspired by the flexibility of types within the httpx library # https://github.com/encode/httpx/blob/master/httpx/_types.py -FileContent = typing.Union[typing.IO[bytes], bytes, str] -File = typing.Union[ +FileContent = Union[IO[bytes], bytes, str] +File = Union[ # file (or bytes) FileContent, # (filename, file (or bytes)) - typing.Tuple[typing.Optional[str], FileContent], + Tuple[Optional[str], FileContent], # (filename, file (or bytes), content_type) - typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str]], + Tuple[Optional[str], FileContent, Optional[str]], # (filename, file (or bytes), content_type, headers) - typing.Tuple[ - typing.Optional[str], + Tuple[ + Optional[str], FileContent, - typing.Optional[str], - typing.Mapping[str, str], + Optional[str], + Mapping[str, str], ], ] def convert_file_dict_to_httpx_tuples( - d: typing.Dict[str, typing.Union[File, typing.List[File]]], -) -> typing.List[typing.Tuple[str, File]]: + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: """ The format we use is a list of tuples, where the first element is the name of the file and the second is the file object. Typically HTTPX wants @@ -41,3 +41,30 @@ def convert_file_dict_to_httpx_tuples( else: httpx_tuples.append((key, file_like)) return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast( + Tuple[Optional[str], FileContent, Optional[str]], file + ) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], + file, + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/langfuse/api/core/force_multipart.py b/langfuse/api/core/force_multipart.py new file mode 100644 index 000000000..5440913fd --- /dev/null +++ b/langfuse/api/core/force_multipart.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict + + +class ForceMultipartDict(Dict[str, Any]): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self) -> bool: + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/langfuse/api/core/http_client.py b/langfuse/api/core/http_client.py index 091f71bc1..3025a49ba 100644 --- a/langfuse/api/core/http_client.py +++ b/langfuse/api/core/http_client.py @@ -2,7 +2,6 @@ import asyncio import email.utils -import json import re import time import typing @@ -11,16 +10,17 @@ from random import random import httpx - from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART from .jsonable_encoder import jsonable_encoder from .query_encoder import encode_query -from .remove_none_from_dict import remove_none_from_dict +from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict from .request_options import RequestOptions +from httpx._types import RequestFiles -INITIAL_RETRY_DELAY_SECONDS = 0.5 -MAX_RETRY_DELAY_SECONDS = 10 -MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 +INITIAL_RETRY_DELAY_SECONDS = 1.0 +MAX_RETRY_DELAY_SECONDS = 60.0 +JITTER_FACTOR = 0.2 # 20% random jitter def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: @@ -64,6 +64,38 @@ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float return seconds +def _add_positive_jitter(delay: float) -> float: + """Add positive jitter (0-20%) to prevent thundering herd.""" + jitter_multiplier = 1 + random() * JITTER_FACTOR + return delay * jitter_multiplier + + +def _add_symmetric_jitter(delay: float) -> float: + """Add symmetric jitter (±10%) for exponential backoff.""" + jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR + return delay * jitter_multiplier + + +def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + Parse the X-RateLimit-Reset header (Unix timestamp in seconds). + Returns seconds to wait, or None if header is missing/invalid. + """ + reset_time_str = response_headers.get("x-ratelimit-reset") + if reset_time_str is None: + return None + + try: + reset_time = int(reset_time_str) + delay = reset_time - time.time() + if delay > 0: + return delay + except (ValueError, TypeError): + pass + + return None + + def _retry_timeout(response: httpx.Response, retries: int) -> float: """ Determine the amount of time to wait before retrying a request. @@ -71,24 +103,45 @@ def _retry_timeout(response: httpx.Response, retries: int) -> float: with a jitter to determine the number of seconds to wait. """ - # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + # 1. Check Retry-After header first retry_after = _parse_retry_after(response.headers) - if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: - return retry_after + if retry_after is not None and retry_after > 0: + return min(retry_after, MAX_RETRY_DELAY_SECONDS) - # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. - retry_delay = min( + # 2. Check X-RateLimit-Reset header (with positive jitter) + ratelimit_reset = _parse_x_ratelimit_reset(response.headers) + if ratelimit_reset is not None: + return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) + + # 3. Fall back to exponential backoff (with symmetric jitter) + backoff = min( INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS ) - - # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. - timeout = retry_delay * (1 - 0.25 * random()) - return timeout if timeout >= 0 else 0 + return _add_symmetric_jitter(backoff) def _should_retry(response: httpx.Response) -> bool: - retriable_400s = [429, 408, 409] - return response.status_code >= 500 or response.status_code in retriable_400s + retryable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retryable_400s + + +def _maybe_filter_none_from_multipart_data( + data: typing.Optional[typing.Any], + request_files: typing.Optional[RequestFiles], + force_multipart: typing.Optional[bool], +) -> typing.Optional[typing.Any]: + """ + Filter None values from data body for multipart/form requests. + This prevents httpx from converting None to empty strings in multipart encoding. + Only applies when files are present or force_multipart is True. + """ + if ( + data is not None + and isinstance(data, typing.Mapping) + and (request_files or force_multipart) + ): + return remove_none_from_dict(data) + return data def remove_omit_from_dict( @@ -147,7 +200,10 @@ def get_request_body( # If both data and json are None, we send json data in the event extra properties are specified json_body = maybe_filter_request_body(json, request_options, omit) - return json_body, data_body + # If you have an empty JSON body, you should just send None + return ( + json_body if json_body != {} else None + ), data_body if data_body != {} else None class HttpClient: @@ -155,9 +211,9 @@ def __init__( self, *, httpx_client: httpx.Client, - base_timeout: typing.Optional[float], - base_headers: typing.Dict[str, str], - base_url: typing.Optional[str] = None, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, ): self.base_url = base_url self.base_timeout = base_timeout @@ -165,7 +221,10 @@ def __init__( self.httpx_client = httpx_client def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: - base_url = self.base_url if maybe_base_url is None else maybe_base_url + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + if base_url is None: raise ValueError( "A base_url is required to make this request, please provide one and try again." @@ -185,32 +244,74 @@ def request( typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]] ] = None, files: typing.Optional[ - typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]] + typing.Union[ + typing.Dict[ + str, typing.Optional[typing.Union[File, typing.List[File]]] + ], + typing.List[typing.Tuple[str, File]], + ] ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, retries: int = 0, omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") if request_options is not None and request_options.get("timeout_in_seconds") is not None - else self.base_timeout + else self.base_timeout() ) json_body, data_body = get_request_body( json=json, data=data, request_options=request_options, omit=omit ) + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples( + remove_omit_from_dict(remove_none_from_dict(files), omit) + ) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + data_body = _maybe_filter_none_from_multipart_data( + data_body, request_files, force_multipart + ) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + response = self.httpx_client.request( method=method, url=urllib.parse.urljoin(f"{base_url}/", path), headers=jsonable_encoder( remove_none_from_dict( { - **self.base_headers, + **self.base_headers(), **(headers if headers is not None else {}), **( request_options.get("additional_headers", {}) or {} @@ -220,40 +321,19 @@ def request( } ) ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get( - "additional_query_parameters", {} - ) - or {} - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ), + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, - files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) - if files is not None - else None, + files=request_files, timeout=timeout, ) max_retries: int = ( - request_options.get("max_retries", 0) if request_options is not None else 0 + request_options.get("max_retries", 2) if request_options is not None else 2 ) if _should_retry(response=response): - if max_retries > retries: + if retries < max_retries: time.sleep(_retry_timeout(response=response, retries=retries)) return self.request( path=path, @@ -285,32 +365,73 @@ def stream( typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]] ] = None, files: typing.Optional[ - typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]] + typing.Union[ + typing.Dict[ + str, typing.Optional[typing.Union[File, typing.List[File]]] + ], + typing.List[typing.Tuple[str, File]], + ] ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, retries: int = 0, omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") if request_options is not None and request_options.get("timeout_in_seconds") is not None - else self.base_timeout + else self.base_timeout() ) + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples( + remove_omit_from_dict(remove_none_from_dict(files), omit) + ) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + json_body, data_body = get_request_body( json=json, data=data, request_options=request_options, omit=omit ) + data_body = _maybe_filter_none_from_multipart_data( + data_body, request_files, force_multipart + ) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + with self.httpx_client.stream( method=method, url=urllib.parse.urljoin(f"{base_url}/", path), headers=jsonable_encoder( remove_none_from_dict( { - **self.base_headers, + **self.base_headers(), **(headers if headers is not None else {}), **( request_options.get("additional_headers", {}) @@ -320,31 +441,11 @@ def stream( } ) ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get( - "additional_query_parameters", {} - ) - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ), + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, - files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) - if files is not None - else None, + files=request_files, timeout=timeout, ) as stream: yield stream @@ -355,17 +456,29 @@ def __init__( self, *, httpx_client: httpx.AsyncClient, - base_timeout: typing.Optional[float], - base_headers: typing.Dict[str, str], - base_url: typing.Optional[str] = None, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + async_base_headers: typing.Optional[ + typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]] + ] = None, ): self.base_url = base_url self.base_timeout = base_timeout self.base_headers = base_headers + self.async_base_headers = async_base_headers self.httpx_client = httpx_client + async def _get_headers(self) -> typing.Dict[str, str]: + if self.async_base_headers is not None: + return await self.async_base_headers() + return self.base_headers() + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: - base_url = self.base_url if maybe_base_url is None else maybe_base_url + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + if base_url is None: raise ValueError( "A base_url is required to make this request, please provide one and try again." @@ -385,25 +498,70 @@ async def request( typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]] ] = None, files: typing.Optional[ - typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]] + typing.Union[ + typing.Dict[ + str, typing.Optional[typing.Union[File, typing.List[File]]] + ], + typing.List[typing.Tuple[str, File]], + ] ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, retries: int = 0, omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") if request_options is not None and request_options.get("timeout_in_seconds") is not None - else self.base_timeout + else self.base_timeout() ) + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples( + remove_omit_from_dict(remove_none_from_dict(files), omit) + ) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + json_body, data_body = get_request_body( json=json, data=data, request_options=request_options, omit=omit ) + data_body = _maybe_filter_none_from_multipart_data( + data_body, request_files, force_multipart + ) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + # Add the input to each of these and do None-safety checks response = await self.httpx_client.request( method=method, @@ -411,7 +569,7 @@ async def request( headers=jsonable_encoder( remove_none_from_dict( { - **self.base_headers, + **_headers, **(headers if headers is not None else {}), **( request_options.get("additional_headers", {}) or {} @@ -421,40 +579,19 @@ async def request( } ) ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get( - "additional_query_parameters", {} - ) - or {} - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ), + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, - files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) - if files is not None - else None, + files=request_files, timeout=timeout, ) max_retries: int = ( - request_options.get("max_retries", 0) if request_options is not None else 0 + request_options.get("max_retries", 2) if request_options is not None else 2 ) if _should_retry(response=response): - if max_retries > retries: + if retries < max_retries: await asyncio.sleep(_retry_timeout(response=response, retries=retries)) return await self.request( path=path, @@ -485,32 +622,76 @@ async def stream( typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]] ] = None, files: typing.Optional[ - typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]] + typing.Union[ + typing.Dict[ + str, typing.Optional[typing.Union[File, typing.List[File]]] + ], + typing.List[typing.Tuple[str, File]], + ] ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, retries: int = 0, omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") if request_options is not None and request_options.get("timeout_in_seconds") is not None - else self.base_timeout + else self.base_timeout() ) + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples( + remove_omit_from_dict(remove_none_from_dict(files), omit) + ) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + json_body, data_body = get_request_body( json=json, data=data, request_options=request_options, omit=omit ) + data_body = _maybe_filter_none_from_multipart_data( + data_body, request_files, force_multipart + ) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ) + async with self.httpx_client.stream( method=method, url=urllib.parse.urljoin(f"{base_url}/", path), headers=jsonable_encoder( remove_none_from_dict( { - **self.base_headers, + **_headers, **(headers if headers is not None else {}), **( request_options.get("additional_headers", {}) @@ -520,31 +701,11 @@ async def stream( } ) ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get( - "additional_query_parameters", {} - ) - if request_options is not None - else {} - ), - }, - omit=omit, - ) - ) - ) - ), + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, - files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) - if files is not None - else None, + files=request_files, timeout=timeout, ) as stream: yield stream diff --git a/langfuse/api/core/http_response.py b/langfuse/api/core/http_response.py new file mode 100644 index 000000000..2479747e8 --- /dev/null +++ b/langfuse/api/core/http_response.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +# Generic to represent the underlying type of the data wrapped by the HTTP response. +T = TypeVar("T") + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/langfuse/api/core/http_sse/__init__.py b/langfuse/api/core/http_sse/__init__.py new file mode 100644 index 000000000..ab0b4995a --- /dev/null +++ b/langfuse/api/core/http_sse/__init__.py @@ -0,0 +1,48 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/langfuse/api/core/http_sse/_api.py b/langfuse/api/core/http_sse/_api.py new file mode 100644 index 000000000..eb739a22b --- /dev/null +++ b/langfuse/api/core/http_sse/_api.py @@ -0,0 +1,114 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse( + client: httpx.Client, method: str, url: str, **kwargs: Any +) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/langfuse/api/core/http_sse/_decoders.py b/langfuse/api/core/http_sse/_decoders.py new file mode 100644 index 000000000..bdec57b44 --- /dev/null +++ b/langfuse/api/core/http_sse/_decoders.py @@ -0,0 +1,66 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if ( + not self._event + and not self._data + and not self._last_event_id + and self._retry is None + ): + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/langfuse/api/resources/utils/resources/pagination/__init__.py b/langfuse/api/core/http_sse/_exceptions.py similarity index 51% rename from langfuse/api/resources/utils/resources/pagination/__init__.py rename to langfuse/api/core/http_sse/_exceptions.py index 9bd1e5f71..81605a8a6 100644 --- a/langfuse/api/resources/utils/resources/pagination/__init__.py +++ b/langfuse/api/core/http_sse/_exceptions.py @@ -1,5 +1,7 @@ # This file was auto-generated by Fern from our API Definition. -from .types import MetaResponse +import httpx -__all__ = ["MetaResponse"] + +class SSEError(httpx.TransportError): + pass diff --git a/langfuse/api/core/http_sse/_models.py b/langfuse/api/core/http_sse/_models.py new file mode 100644 index 000000000..1af57f8fd --- /dev/null +++ b/langfuse/api/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/langfuse/api/core/jsonable_encoder.py b/langfuse/api/core/jsonable_encoder.py index 7a05e9190..90f53dfa7 100644 --- a/langfuse/api/core/jsonable_encoder.py +++ b/langfuse/api/core/jsonable_encoder.py @@ -11,35 +11,23 @@ import base64 import dataclasses import datetime as dt -from collections import defaultdict from enum import Enum from pathlib import PurePath from types import GeneratorType -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Set, Union +import pydantic from .datetime_utils import serialize_datetime -from .pydantic_utilities import pydantic_v1 +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) SetIntStr = Set[Union[int, str]] DictIntStrAny = Dict[Union[int, str], Any] -def generate_encoders_by_class_tuples( - type_encoder_map: Dict[Any, Callable[[Any], Any]], -) -> Dict[Callable[[Any], Any], Tuple[Any, ...]]: - encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict( - tuple - ) - for type_, encoder in type_encoder_map.items(): - encoders_by_class_tuples[encoder] += (type_,) - return encoders_by_class_tuples - - -encoders_by_class_tuples = generate_encoders_by_class_tuples( - pydantic_v1.json.ENCODERS_BY_TYPE -) - - def jsonable_encoder( obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None ) -> Any: @@ -51,16 +39,21 @@ def jsonable_encoder( for encoder_type, encoder_instance in custom_encoder.items(): if isinstance(obj, encoder_type): return encoder_instance(obj) - if isinstance(obj, pydantic_v1.BaseModel): - encoder = getattr(obj.__config__, "json_encoders", {}) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 if custom_encoder: encoder.update(custom_encoder) obj_dict = obj.dict(by_alias=True) if "__root__" in obj_dict: obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] return jsonable_encoder(obj_dict, custom_encoder=encoder) if dataclasses.is_dataclass(obj): - obj_dict = dataclasses.asdict(obj) + obj_dict = dataclasses.asdict(obj) # type: ignore return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) if isinstance(obj, bytes): return base64.b64encode(obj).decode("utf-8") @@ -89,20 +82,21 @@ def jsonable_encoder( encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) return encoded_list - if type(obj) in pydantic_v1.json.ENCODERS_BY_TYPE: - return pydantic_v1.json.ENCODERS_BY_TYPE[type(obj)](obj) - for encoder, classes_tuple in encoders_by_class_tuples.items(): - if isinstance(obj, classes_tuple): - return encoder(obj) + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode - try: - data = dict(obj) - except Exception as e: - errors: List[Exception] = [] - errors.append(e) try: - data = vars(obj) + data = dict(o) except Exception as e: + errors: List[Exception] = [] errors.append(e) - raise ValueError(errors) from e - return jsonable_encoder(data, custom_encoder=custom_encoder) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) diff --git a/langfuse/api/core/pydantic_utilities.py b/langfuse/api/core/pydantic_utilities.py index a72c1a52f..d2b7b51b6 100644 --- a/langfuse/api/core/pydantic_utilities.py +++ b/langfuse/api/core/pydantic_utilities.py @@ -1,28 +1,310 @@ # This file was auto-generated by Fern from our API Definition. -import typing +# nopycln: file +import datetime as dt +from collections import defaultdict +from typing import ( + Any, + Callable, + ClassVar, + Dict, + List, + Mapping, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) import pydantic IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") if IS_PYDANTIC_V2: - import pydantic.v1 as pydantic_v1 # type: ignore # nopycln: import + from pydantic.v1.datetime_parse import parse_date as parse_date + from pydantic.v1.datetime_parse import parse_datetime as parse_datetime + from pydantic.v1.fields import ModelField as ModelField + from pydantic.v1.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[attr-defined] + from pydantic.v1.typing import get_args as get_args + from pydantic.v1.typing import get_origin as get_origin + from pydantic.v1.typing import is_literal_type as is_literal_type + from pydantic.v1.typing import is_union as is_union else: - import pydantic as pydantic_v1 # type: ignore # nopycln: import + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + dealiased_object = convert_and_respect_annotation_metadata( + object_=object_, annotation=type_, direction="read" + ) + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + return adapter.validate_python(dealiased_object) + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback( + obj: Any, fallback_serializer: Callable[[Any], Any] +) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.dict() # type: ignore[attr-defined] + data = { + k: serialize_datetime(v) if isinstance(v, dt.datetime) else v + for k, v in serialized.items() + } + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @classmethod + def model_construct( + cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any + ) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata( + object_=values, annotation=cls, direction="read" + ) + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct( + cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any + ) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata( + object_=values, annotation=cls, direction="read" + ) + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ( + "exclude_unset" in kwargs and not kwargs["exclude_unset"] + ): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict( + **kwargs_with_defaults_exclude_unset_include_fields + ) + + return cast( + Dict[str, Any], + convert_and_respect_annotation_metadata( + object_=dict_dump, annotation=self.__class__, direction="write" + ), + ) + + +def _union_list_of_pydantic_dicts( + source: List[Any], destination: List[Any] +) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append( + _union_list_of_pydantic_dicts(item, destination_value) + ) + else: + converted_list.append(item) + return converted_list def deep_union_pydantic_dicts( - source: typing.Dict[str, typing.Any], destination: typing.Dict[str, typing.Any] -) -> typing.Dict[str, typing.Any]: + source: Dict[str, Any], destination: Dict[str, Any] +) -> Dict[str, Any]: for key, value in source.items(): + node = destination.setdefault(key, {}) if isinstance(value, dict): - node = destination.setdefault(key, {}) deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) else: destination[key] = value return destination -__all__ = ["pydantic_v1"] +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict( + tuple + ) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + # In Pydantic v2, for RootModel we always use "before" mode + # The custom validators transform the input value before the model is created + return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator( + field_name: str, pre: bool = False +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast( + AnyCallable, + pydantic.field_validator(field_name, mode="before" if pre else "after")( + func + ), + ) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, pydantic.fields.FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/langfuse/api/core/query_encoder.py b/langfuse/api/core/query_encoder.py index 069633086..03fbf59bd 100644 --- a/langfuse/api/core/query_encoder.py +++ b/langfuse/api/core/query_encoder.py @@ -1,39 +1,60 @@ # This file was auto-generated by Fern from our API Definition. -from collections import ChainMap -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple -from .pydantic_utilities import pydantic_v1 +import pydantic # Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict def traverse_query_dict( dict_flat: Dict[str, Any], key_prefix: Optional[str] = None -) -> Dict[str, Any]: - result = {} +) -> List[Tuple[str, Any]]: + result = [] for k, v in dict_flat.items(): key = f"{key_prefix}[{k}]" if key_prefix is not None else k if isinstance(v, dict): - result.update(traverse_query_dict(v, key)) + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) else: - result[key] = v + result.append((key, v)) return result -def single_query_encoder(query_key: str, query_value: Any) -> Dict[str, Any]: - if isinstance(query_value, pydantic_v1.BaseModel) or isinstance(query_value, dict): - if isinstance(query_value, pydantic_v1.BaseModel): +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): obj_dict = query_value.dict(by_alias=True) else: obj_dict = query_value return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value - return {query_key: query_value} + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + return encoded_values -def encode_query(query: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: - return ( - dict(ChainMap(*[single_query_encoder(k, v) for k, v in query.items()])) - if query is not None - else None - ) + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/langfuse/api/core/request_options.py b/langfuse/api/core/request_options.py index d0bf0dbce..1b3880443 100644 --- a/langfuse/api/core/request_options.py +++ b/langfuse/api/core/request_options.py @@ -23,6 +23,8 @@ class RequestOptions(typing.TypedDict, total=False): - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. """ timeout_in_seconds: NotRequired[int] @@ -30,3 +32,4 @@ class RequestOptions(typing.TypedDict, total=False): additional_headers: NotRequired[typing.Dict[str, typing.Any]] additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] diff --git a/langfuse/api/core/serialization.py b/langfuse/api/core/serialization.py new file mode 100644 index 000000000..ad6eb8d7f --- /dev/null +++ b/langfuse/api/core/serialization.py @@ -0,0 +1,282 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance( + object_, typing.Mapping + ): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + try: + annotations = typing_extensions.get_type_hints( + expected_type, include_extras=True + ) + except NameError: + # The TypedDict contains a circular reference, so + # we use the __annotations__ attribute directly. + annotations = getattr(expected_type, "__annotations__", {}) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[ + _alias_key(key, type_, direction, aliases_to_field_names) + ] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/langfuse/api/dataset_items/__init__.py b/langfuse/api/dataset_items/__init__.py new file mode 100644 index 000000000..4d009d228 --- /dev/null +++ b/langfuse/api/dataset_items/__init__.py @@ -0,0 +1,52 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + CreateDatasetItemRequest, + DeleteDatasetItemResponse, + PaginatedDatasetItems, + ) +_dynamic_imports: typing.Dict[str, str] = { + "CreateDatasetItemRequest": ".types", + "DeleteDatasetItemResponse": ".types", + "PaginatedDatasetItems": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CreateDatasetItemRequest", + "DeleteDatasetItemResponse", + "PaginatedDatasetItems", +] diff --git a/langfuse/api/dataset_items/client.py b/langfuse/api/dataset_items/client.py new file mode 100644 index 000000000..c44ca24d8 --- /dev/null +++ b/langfuse/api/dataset_items/client.py @@ -0,0 +1,499 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..commons.types.dataset_item import DatasetItem +from ..commons.types.dataset_status import DatasetStatus +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawDatasetItemsClient, RawDatasetItemsClient +from .types.delete_dataset_item_response import DeleteDatasetItemResponse +from .types.paginated_dataset_items import PaginatedDatasetItems + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class DatasetItemsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawDatasetItemsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawDatasetItemsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawDatasetItemsClient + """ + return self._raw_client + + def create( + self, + *, + dataset_name: str, + input: typing.Optional[typing.Any] = OMIT, + expected_output: typing.Optional[typing.Any] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + source_trace_id: typing.Optional[str] = OMIT, + source_observation_id: typing.Optional[str] = OMIT, + id: typing.Optional[str] = OMIT, + status: typing.Optional[DatasetStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> DatasetItem: + """ + Create a dataset item + + Parameters + ---------- + dataset_name : str + + input : typing.Optional[typing.Any] + + expected_output : typing.Optional[typing.Any] + + metadata : typing.Optional[typing.Any] + + source_trace_id : typing.Optional[str] + + source_observation_id : typing.Optional[str] + + id : typing.Optional[str] + Dataset items are upserted on their id. Id needs to be unique (project-level) and cannot be reused across datasets. + + status : typing.Optional[DatasetStatus] + Defaults to ACTIVE for newly created items + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DatasetItem + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.dataset_items.create( + dataset_name="datasetName", + ) + """ + _response = self._raw_client.create( + dataset_name=dataset_name, + input=input, + expected_output=expected_output, + metadata=metadata, + source_trace_id=source_trace_id, + source_observation_id=source_observation_id, + id=id, + status=status, + request_options=request_options, + ) + return _response.data + + def get( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> DatasetItem: + """ + Get a dataset item + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DatasetItem + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.dataset_items.get( + id="id", + ) + """ + _response = self._raw_client.get(id, request_options=request_options) + return _response.data + + def list( + self, + *, + dataset_name: typing.Optional[str] = None, + source_trace_id: typing.Optional[str] = None, + source_observation_id: typing.Optional[str] = None, + version: typing.Optional[dt.datetime] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedDatasetItems: + """ + Get dataset items. Optionally specify a version to get the items as they existed at that point in time. + Note: If version parameter is provided, datasetName must also be provided. + + Parameters + ---------- + dataset_name : typing.Optional[str] + + source_trace_id : typing.Optional[str] + + source_observation_id : typing.Optional[str] + + version : typing.Optional[dt.datetime] + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + If provided, returns state of dataset at this timestamp. + If not provided, returns the latest version. Requires datasetName to be specified. + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedDatasetItems + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.dataset_items.list() + """ + _response = self._raw_client.list( + dataset_name=dataset_name, + source_trace_id=source_trace_id, + source_observation_id=source_observation_id, + version=version, + page=page, + limit=limit, + request_options=request_options, + ) + return _response.data + + def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteDatasetItemResponse: + """ + Delete a dataset item and all its run items. This action is irreversible. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteDatasetItemResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.dataset_items.delete( + id="id", + ) + """ + _response = self._raw_client.delete(id, request_options=request_options) + return _response.data + + +class AsyncDatasetItemsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawDatasetItemsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawDatasetItemsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawDatasetItemsClient + """ + return self._raw_client + + async def create( + self, + *, + dataset_name: str, + input: typing.Optional[typing.Any] = OMIT, + expected_output: typing.Optional[typing.Any] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + source_trace_id: typing.Optional[str] = OMIT, + source_observation_id: typing.Optional[str] = OMIT, + id: typing.Optional[str] = OMIT, + status: typing.Optional[DatasetStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> DatasetItem: + """ + Create a dataset item + + Parameters + ---------- + dataset_name : str + + input : typing.Optional[typing.Any] + + expected_output : typing.Optional[typing.Any] + + metadata : typing.Optional[typing.Any] + + source_trace_id : typing.Optional[str] + + source_observation_id : typing.Optional[str] + + id : typing.Optional[str] + Dataset items are upserted on their id. Id needs to be unique (project-level) and cannot be reused across datasets. + + status : typing.Optional[DatasetStatus] + Defaults to ACTIVE for newly created items + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DatasetItem + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.dataset_items.create( + dataset_name="datasetName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + dataset_name=dataset_name, + input=input, + expected_output=expected_output, + metadata=metadata, + source_trace_id=source_trace_id, + source_observation_id=source_observation_id, + id=id, + status=status, + request_options=request_options, + ) + return _response.data + + async def get( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> DatasetItem: + """ + Get a dataset item + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DatasetItem + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.dataset_items.get( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(id, request_options=request_options) + return _response.data + + async def list( + self, + *, + dataset_name: typing.Optional[str] = None, + source_trace_id: typing.Optional[str] = None, + source_observation_id: typing.Optional[str] = None, + version: typing.Optional[dt.datetime] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedDatasetItems: + """ + Get dataset items. Optionally specify a version to get the items as they existed at that point in time. + Note: If version parameter is provided, datasetName must also be provided. + + Parameters + ---------- + dataset_name : typing.Optional[str] + + source_trace_id : typing.Optional[str] + + source_observation_id : typing.Optional[str] + + version : typing.Optional[dt.datetime] + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + If provided, returns state of dataset at this timestamp. + If not provided, returns the latest version. Requires datasetName to be specified. + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedDatasetItems + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.dataset_items.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + dataset_name=dataset_name, + source_trace_id=source_trace_id, + source_observation_id=source_observation_id, + version=version, + page=page, + limit=limit, + request_options=request_options, + ) + return _response.data + + async def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteDatasetItemResponse: + """ + Delete a dataset item and all its run items. This action is irreversible. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteDatasetItemResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.dataset_items.delete( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(id, request_options=request_options) + return _response.data diff --git a/langfuse/api/dataset_items/raw_client.py b/langfuse/api/dataset_items/raw_client.py new file mode 100644 index 000000000..6aeafb54d --- /dev/null +++ b/langfuse/api/dataset_items/raw_client.py @@ -0,0 +1,973 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.dataset_item import DatasetItem +from ..commons.types.dataset_status import DatasetStatus +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.delete_dataset_item_response import DeleteDatasetItemResponse +from .types.paginated_dataset_items import PaginatedDatasetItems + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawDatasetItemsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + dataset_name: str, + input: typing.Optional[typing.Any] = OMIT, + expected_output: typing.Optional[typing.Any] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + source_trace_id: typing.Optional[str] = OMIT, + source_observation_id: typing.Optional[str] = OMIT, + id: typing.Optional[str] = OMIT, + status: typing.Optional[DatasetStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DatasetItem]: + """ + Create a dataset item + + Parameters + ---------- + dataset_name : str + + input : typing.Optional[typing.Any] + + expected_output : typing.Optional[typing.Any] + + metadata : typing.Optional[typing.Any] + + source_trace_id : typing.Optional[str] + + source_observation_id : typing.Optional[str] + + id : typing.Optional[str] + Dataset items are upserted on their id. Id needs to be unique (project-level) and cannot be reused across datasets. + + status : typing.Optional[DatasetStatus] + Defaults to ACTIVE for newly created items + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DatasetItem] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/dataset-items", + method="POST", + json={ + "datasetName": dataset_name, + "input": input, + "expectedOutput": expected_output, + "metadata": metadata, + "sourceTraceId": source_trace_id, + "sourceObservationId": source_observation_id, + "id": id, + "status": status, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DatasetItem, + parse_obj_as( + type_=DatasetItem, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[DatasetItem]: + """ + Get a dataset item + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DatasetItem] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/dataset-items/{jsonable_encoder(id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DatasetItem, + parse_obj_as( + type_=DatasetItem, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def list( + self, + *, + dataset_name: typing.Optional[str] = None, + source_trace_id: typing.Optional[str] = None, + source_observation_id: typing.Optional[str] = None, + version: typing.Optional[dt.datetime] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PaginatedDatasetItems]: + """ + Get dataset items. Optionally specify a version to get the items as they existed at that point in time. + Note: If version parameter is provided, datasetName must also be provided. + + Parameters + ---------- + dataset_name : typing.Optional[str] + + source_trace_id : typing.Optional[str] + + source_observation_id : typing.Optional[str] + + version : typing.Optional[dt.datetime] + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + If provided, returns state of dataset at this timestamp. + If not provided, returns the latest version. Requires datasetName to be specified. + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedDatasetItems] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/dataset-items", + method="GET", + params={ + "datasetName": dataset_name, + "sourceTraceId": source_trace_id, + "sourceObservationId": source_observation_id, + "version": serialize_datetime(version) if version is not None else None, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedDatasetItems, + parse_obj_as( + type_=PaginatedDatasetItems, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[DeleteDatasetItemResponse]: + """ + Delete a dataset item and all its run items. This action is irreversible. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteDatasetItemResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/dataset-items/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteDatasetItemResponse, + parse_obj_as( + type_=DeleteDatasetItemResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawDatasetItemsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + dataset_name: str, + input: typing.Optional[typing.Any] = OMIT, + expected_output: typing.Optional[typing.Any] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + source_trace_id: typing.Optional[str] = OMIT, + source_observation_id: typing.Optional[str] = OMIT, + id: typing.Optional[str] = OMIT, + status: typing.Optional[DatasetStatus] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DatasetItem]: + """ + Create a dataset item + + Parameters + ---------- + dataset_name : str + + input : typing.Optional[typing.Any] + + expected_output : typing.Optional[typing.Any] + + metadata : typing.Optional[typing.Any] + + source_trace_id : typing.Optional[str] + + source_observation_id : typing.Optional[str] + + id : typing.Optional[str] + Dataset items are upserted on their id. Id needs to be unique (project-level) and cannot be reused across datasets. + + status : typing.Optional[DatasetStatus] + Defaults to ACTIVE for newly created items + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DatasetItem] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/dataset-items", + method="POST", + json={ + "datasetName": dataset_name, + "input": input, + "expectedOutput": expected_output, + "metadata": metadata, + "sourceTraceId": source_trace_id, + "sourceObservationId": source_observation_id, + "id": id, + "status": status, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DatasetItem, + parse_obj_as( + type_=DatasetItem, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[DatasetItem]: + """ + Get a dataset item + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DatasetItem] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/dataset-items/{jsonable_encoder(id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DatasetItem, + parse_obj_as( + type_=DatasetItem, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def list( + self, + *, + dataset_name: typing.Optional[str] = None, + source_trace_id: typing.Optional[str] = None, + source_observation_id: typing.Optional[str] = None, + version: typing.Optional[dt.datetime] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PaginatedDatasetItems]: + """ + Get dataset items. Optionally specify a version to get the items as they existed at that point in time. + Note: If version parameter is provided, datasetName must also be provided. + + Parameters + ---------- + dataset_name : typing.Optional[str] + + source_trace_id : typing.Optional[str] + + source_observation_id : typing.Optional[str] + + version : typing.Optional[dt.datetime] + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + If provided, returns state of dataset at this timestamp. + If not provided, returns the latest version. Requires datasetName to be specified. + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedDatasetItems] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/dataset-items", + method="GET", + params={ + "datasetName": dataset_name, + "sourceTraceId": source_trace_id, + "sourceObservationId": source_observation_id, + "version": serialize_datetime(version) if version is not None else None, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedDatasetItems, + parse_obj_as( + type_=PaginatedDatasetItems, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[DeleteDatasetItemResponse]: + """ + Delete a dataset item and all its run items. This action is irreversible. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteDatasetItemResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/dataset-items/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteDatasetItemResponse, + parse_obj_as( + type_=DeleteDatasetItemResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/dataset_items/types/__init__.py b/langfuse/api/dataset_items/types/__init__.py new file mode 100644 index 000000000..c7ce59bf4 --- /dev/null +++ b/langfuse/api/dataset_items/types/__init__.py @@ -0,0 +1,50 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_dataset_item_request import CreateDatasetItemRequest + from .delete_dataset_item_response import DeleteDatasetItemResponse + from .paginated_dataset_items import PaginatedDatasetItems +_dynamic_imports: typing.Dict[str, str] = { + "CreateDatasetItemRequest": ".create_dataset_item_request", + "DeleteDatasetItemResponse": ".delete_dataset_item_response", + "PaginatedDatasetItems": ".paginated_dataset_items", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CreateDatasetItemRequest", + "DeleteDatasetItemResponse", + "PaginatedDatasetItems", +] diff --git a/langfuse/api/dataset_items/types/create_dataset_item_request.py b/langfuse/api/dataset_items/types/create_dataset_item_request.py new file mode 100644 index 000000000..b778e42ae --- /dev/null +++ b/langfuse/api/dataset_items/types/create_dataset_item_request.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...commons.types.dataset_status import DatasetStatus +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class CreateDatasetItemRequest(UniversalBaseModel): + dataset_name: typing_extensions.Annotated[str, FieldMetadata(alias="datasetName")] + input: typing.Optional[typing.Any] = None + expected_output: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="expectedOutput") + ] = None + metadata: typing.Optional[typing.Any] = None + source_trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sourceTraceId") + ] = None + source_observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sourceObservationId") + ] = None + id: typing.Optional[str] = pydantic.Field(default=None) + """ + Dataset items are upserted on their id. Id needs to be unique (project-level) and cannot be reused across datasets. + """ + + status: typing.Optional[DatasetStatus] = pydantic.Field(default=None) + """ + Defaults to ACTIVE for newly created items + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/dataset_items/types/delete_dataset_item_response.py b/langfuse/api/dataset_items/types/delete_dataset_item_response.py new file mode 100644 index 000000000..982e4f2dd --- /dev/null +++ b/langfuse/api/dataset_items/types/delete_dataset_item_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class DeleteDatasetItemResponse(UniversalBaseModel): + message: str = pydantic.Field() + """ + Success message after deletion + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/dataset_items/types/paginated_dataset_items.py b/langfuse/api/dataset_items/types/paginated_dataset_items.py new file mode 100644 index 000000000..63a683787 --- /dev/null +++ b/langfuse/api/dataset_items/types/paginated_dataset_items.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.dataset_item import DatasetItem +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse + + +class PaginatedDatasetItems(UniversalBaseModel): + data: typing.List[DatasetItem] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/dataset_run_items/__init__.py b/langfuse/api/dataset_run_items/__init__.py new file mode 100644 index 000000000..9ff4e097e --- /dev/null +++ b/langfuse/api/dataset_run_items/__init__.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import CreateDatasetRunItemRequest, PaginatedDatasetRunItems +_dynamic_imports: typing.Dict[str, str] = { + "CreateDatasetRunItemRequest": ".types", + "PaginatedDatasetRunItems": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateDatasetRunItemRequest", "PaginatedDatasetRunItems"] diff --git a/langfuse/api/dataset_run_items/client.py b/langfuse/api/dataset_run_items/client.py new file mode 100644 index 000000000..aee820da8 --- /dev/null +++ b/langfuse/api/dataset_run_items/client.py @@ -0,0 +1,333 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..commons.types.dataset_run_item import DatasetRunItem +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawDatasetRunItemsClient, RawDatasetRunItemsClient +from .types.paginated_dataset_run_items import PaginatedDatasetRunItems + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class DatasetRunItemsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawDatasetRunItemsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawDatasetRunItemsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawDatasetRunItemsClient + """ + return self._raw_client + + def create( + self, + *, + run_name: str, + dataset_item_id: str, + run_description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + observation_id: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + dataset_version: typing.Optional[dt.datetime] = OMIT, + created_at: typing.Optional[dt.datetime] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> DatasetRunItem: + """ + Create a dataset run item + + Parameters + ---------- + run_name : str + + dataset_item_id : str + + run_description : typing.Optional[str] + Description of the run. If run exists, description will be updated. + + metadata : typing.Optional[typing.Any] + Metadata of the dataset run, updates run if run already exists + + observation_id : typing.Optional[str] + + trace_id : typing.Optional[str] + traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. + + dataset_version : typing.Optional[dt.datetime] + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + Specifies the dataset version to use for this experiment run. + If provided, the experiment will use dataset items as they existed at or before this timestamp. + If not provided, uses the latest version of dataset items. + + created_at : typing.Optional[dt.datetime] + Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DatasetRunItem + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.dataset_run_items.create( + run_name="runName", + dataset_item_id="datasetItemId", + ) + """ + _response = self._raw_client.create( + run_name=run_name, + dataset_item_id=dataset_item_id, + run_description=run_description, + metadata=metadata, + observation_id=observation_id, + trace_id=trace_id, + dataset_version=dataset_version, + created_at=created_at, + request_options=request_options, + ) + return _response.data + + def list( + self, + *, + dataset_id: str, + run_name: str, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedDatasetRunItems: + """ + List dataset run items + + Parameters + ---------- + dataset_id : str + + run_name : str + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedDatasetRunItems + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.dataset_run_items.list( + dataset_id="datasetId", + run_name="runName", + ) + """ + _response = self._raw_client.list( + dataset_id=dataset_id, + run_name=run_name, + page=page, + limit=limit, + request_options=request_options, + ) + return _response.data + + +class AsyncDatasetRunItemsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawDatasetRunItemsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawDatasetRunItemsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawDatasetRunItemsClient + """ + return self._raw_client + + async def create( + self, + *, + run_name: str, + dataset_item_id: str, + run_description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + observation_id: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + dataset_version: typing.Optional[dt.datetime] = OMIT, + created_at: typing.Optional[dt.datetime] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> DatasetRunItem: + """ + Create a dataset run item + + Parameters + ---------- + run_name : str + + dataset_item_id : str + + run_description : typing.Optional[str] + Description of the run. If run exists, description will be updated. + + metadata : typing.Optional[typing.Any] + Metadata of the dataset run, updates run if run already exists + + observation_id : typing.Optional[str] + + trace_id : typing.Optional[str] + traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. + + dataset_version : typing.Optional[dt.datetime] + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + Specifies the dataset version to use for this experiment run. + If provided, the experiment will use dataset items as they existed at or before this timestamp. + If not provided, uses the latest version of dataset items. + + created_at : typing.Optional[dt.datetime] + Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DatasetRunItem + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.dataset_run_items.create( + run_name="runName", + dataset_item_id="datasetItemId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + run_name=run_name, + dataset_item_id=dataset_item_id, + run_description=run_description, + metadata=metadata, + observation_id=observation_id, + trace_id=trace_id, + dataset_version=dataset_version, + created_at=created_at, + request_options=request_options, + ) + return _response.data + + async def list( + self, + *, + dataset_id: str, + run_name: str, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedDatasetRunItems: + """ + List dataset run items + + Parameters + ---------- + dataset_id : str + + run_name : str + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedDatasetRunItems + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.dataset_run_items.list( + dataset_id="datasetId", + run_name="runName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + dataset_id=dataset_id, + run_name=run_name, + page=page, + limit=limit, + request_options=request_options, + ) + return _response.data diff --git a/langfuse/api/dataset_run_items/raw_client.py b/langfuse/api/dataset_run_items/raw_client.py new file mode 100644 index 000000000..f281b13d2 --- /dev/null +++ b/langfuse/api/dataset_run_items/raw_client.py @@ -0,0 +1,557 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.dataset_run_item import DatasetRunItem +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.paginated_dataset_run_items import PaginatedDatasetRunItems + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawDatasetRunItemsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + run_name: str, + dataset_item_id: str, + run_description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + observation_id: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + dataset_version: typing.Optional[dt.datetime] = OMIT, + created_at: typing.Optional[dt.datetime] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DatasetRunItem]: + """ + Create a dataset run item + + Parameters + ---------- + run_name : str + + dataset_item_id : str + + run_description : typing.Optional[str] + Description of the run. If run exists, description will be updated. + + metadata : typing.Optional[typing.Any] + Metadata of the dataset run, updates run if run already exists + + observation_id : typing.Optional[str] + + trace_id : typing.Optional[str] + traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. + + dataset_version : typing.Optional[dt.datetime] + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + Specifies the dataset version to use for this experiment run. + If provided, the experiment will use dataset items as they existed at or before this timestamp. + If not provided, uses the latest version of dataset items. + + created_at : typing.Optional[dt.datetime] + Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DatasetRunItem] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/dataset-run-items", + method="POST", + json={ + "runName": run_name, + "runDescription": run_description, + "metadata": metadata, + "datasetItemId": dataset_item_id, + "observationId": observation_id, + "traceId": trace_id, + "datasetVersion": dataset_version, + "createdAt": created_at, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DatasetRunItem, + parse_obj_as( + type_=DatasetRunItem, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def list( + self, + *, + dataset_id: str, + run_name: str, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PaginatedDatasetRunItems]: + """ + List dataset run items + + Parameters + ---------- + dataset_id : str + + run_name : str + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedDatasetRunItems] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/dataset-run-items", + method="GET", + params={ + "datasetId": dataset_id, + "runName": run_name, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedDatasetRunItems, + parse_obj_as( + type_=PaginatedDatasetRunItems, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawDatasetRunItemsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + run_name: str, + dataset_item_id: str, + run_description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + observation_id: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + dataset_version: typing.Optional[dt.datetime] = OMIT, + created_at: typing.Optional[dt.datetime] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DatasetRunItem]: + """ + Create a dataset run item + + Parameters + ---------- + run_name : str + + dataset_item_id : str + + run_description : typing.Optional[str] + Description of the run. If run exists, description will be updated. + + metadata : typing.Optional[typing.Any] + Metadata of the dataset run, updates run if run already exists + + observation_id : typing.Optional[str] + + trace_id : typing.Optional[str] + traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. + + dataset_version : typing.Optional[dt.datetime] + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + Specifies the dataset version to use for this experiment run. + If provided, the experiment will use dataset items as they existed at or before this timestamp. + If not provided, uses the latest version of dataset items. + + created_at : typing.Optional[dt.datetime] + Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DatasetRunItem] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/dataset-run-items", + method="POST", + json={ + "runName": run_name, + "runDescription": run_description, + "metadata": metadata, + "datasetItemId": dataset_item_id, + "observationId": observation_id, + "traceId": trace_id, + "datasetVersion": dataset_version, + "createdAt": created_at, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DatasetRunItem, + parse_obj_as( + type_=DatasetRunItem, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def list( + self, + *, + dataset_id: str, + run_name: str, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PaginatedDatasetRunItems]: + """ + List dataset run items + + Parameters + ---------- + dataset_id : str + + run_name : str + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedDatasetRunItems] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/dataset-run-items", + method="GET", + params={ + "datasetId": dataset_id, + "runName": run_name, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedDatasetRunItems, + parse_obj_as( + type_=PaginatedDatasetRunItems, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/dataset_run_items/types/__init__.py b/langfuse/api/dataset_run_items/types/__init__.py new file mode 100644 index 000000000..b520924c0 --- /dev/null +++ b/langfuse/api/dataset_run_items/types/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_dataset_run_item_request import CreateDatasetRunItemRequest + from .paginated_dataset_run_items import PaginatedDatasetRunItems +_dynamic_imports: typing.Dict[str, str] = { + "CreateDatasetRunItemRequest": ".create_dataset_run_item_request", + "PaginatedDatasetRunItems": ".paginated_dataset_run_items", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateDatasetRunItemRequest", "PaginatedDatasetRunItems"] diff --git a/langfuse/api/dataset_run_items/types/create_dataset_run_item_request.py b/langfuse/api/dataset_run_items/types/create_dataset_run_item_request.py new file mode 100644 index 000000000..169888912 --- /dev/null +++ b/langfuse/api/dataset_run_items/types/create_dataset_run_item_request.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class CreateDatasetRunItemRequest(UniversalBaseModel): + run_name: typing_extensions.Annotated[str, FieldMetadata(alias="runName")] + run_description: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="runDescription") + ] = pydantic.Field(default=None) + """ + Description of the run. If run exists, description will be updated. + """ + + metadata: typing.Optional[typing.Any] = pydantic.Field(default=None) + """ + Metadata of the dataset run, updates run if run already exists + """ + + dataset_item_id: typing_extensions.Annotated[ + str, FieldMetadata(alias="datasetItemId") + ] + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = pydantic.Field(default=None) + """ + traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. + """ + + dataset_version: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="datasetVersion") + ] = pydantic.Field(default=None) + """ + ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). + Specifies the dataset version to use for this experiment run. + If provided, the experiment will use dataset items as they existed at or before this timestamp. + If not provided, uses the latest version of dataset items. + """ + + created_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="createdAt") + ] = pydantic.Field(default=None) + """ + Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/dataset_run_items/types/paginated_dataset_run_items.py b/langfuse/api/dataset_run_items/types/paginated_dataset_run_items.py new file mode 100644 index 000000000..24f7c1996 --- /dev/null +++ b/langfuse/api/dataset_run_items/types/paginated_dataset_run_items.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.dataset_run_item import DatasetRunItem +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse + + +class PaginatedDatasetRunItems(UniversalBaseModel): + data: typing.List[DatasetRunItem] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/datasets/__init__.py b/langfuse/api/datasets/__init__.py new file mode 100644 index 000000000..e9005285e --- /dev/null +++ b/langfuse/api/datasets/__init__.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + CreateDatasetRequest, + DeleteDatasetRunResponse, + PaginatedDatasetRuns, + PaginatedDatasets, + ) +_dynamic_imports: typing.Dict[str, str] = { + "CreateDatasetRequest": ".types", + "DeleteDatasetRunResponse": ".types", + "PaginatedDatasetRuns": ".types", + "PaginatedDatasets": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CreateDatasetRequest", + "DeleteDatasetRunResponse", + "PaginatedDatasetRuns", + "PaginatedDatasets", +] diff --git a/langfuse/api/datasets/client.py b/langfuse/api/datasets/client.py new file mode 100644 index 000000000..859ee0fe7 --- /dev/null +++ b/langfuse/api/datasets/client.py @@ -0,0 +1,661 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..commons.types.dataset import Dataset +from ..commons.types.dataset_run_with_items import DatasetRunWithItems +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawDatasetsClient, RawDatasetsClient +from .types.delete_dataset_run_response import DeleteDatasetRunResponse +from .types.paginated_dataset_runs import PaginatedDatasetRuns +from .types.paginated_datasets import PaginatedDatasets + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class DatasetsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawDatasetsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawDatasetsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawDatasetsClient + """ + return self._raw_client + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedDatasets: + """ + Get all datasets + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedDatasets + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.datasets.list() + """ + _response = self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + def get( + self, + dataset_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> Dataset: + """ + Get a dataset + + Parameters + ---------- + dataset_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Dataset + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.datasets.get( + dataset_name="datasetName", + ) + """ + _response = self._raw_client.get(dataset_name, request_options=request_options) + return _response.data + + def create( + self, + *, + name: str, + description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + input_schema: typing.Optional[typing.Any] = OMIT, + expected_output_schema: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Dataset: + """ + Create a dataset + + Parameters + ---------- + name : str + + description : typing.Optional[str] + + metadata : typing.Optional[typing.Any] + + input_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item inputs. When set, all new and existing dataset items will be validated against this schema. + + expected_output_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item expected outputs. When set, all new and existing dataset items will be validated against this schema. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Dataset + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.datasets.create( + name="name", + ) + """ + _response = self._raw_client.create( + name=name, + description=description, + metadata=metadata, + input_schema=input_schema, + expected_output_schema=expected_output_schema, + request_options=request_options, + ) + return _response.data + + def get_run( + self, + dataset_name: str, + run_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DatasetRunWithItems: + """ + Get a dataset run and its items + + Parameters + ---------- + dataset_name : str + + run_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DatasetRunWithItems + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.datasets.get_run( + dataset_name="datasetName", + run_name="runName", + ) + """ + _response = self._raw_client.get_run( + dataset_name, run_name, request_options=request_options + ) + return _response.data + + def delete_run( + self, + dataset_name: str, + run_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteDatasetRunResponse: + """ + Delete a dataset run and all its run items. This action is irreversible. + + Parameters + ---------- + dataset_name : str + + run_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteDatasetRunResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.datasets.delete_run( + dataset_name="datasetName", + run_name="runName", + ) + """ + _response = self._raw_client.delete_run( + dataset_name, run_name, request_options=request_options + ) + return _response.data + + def get_runs( + self, + dataset_name: str, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedDatasetRuns: + """ + Get dataset runs + + Parameters + ---------- + dataset_name : str + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedDatasetRuns + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.datasets.get_runs( + dataset_name="datasetName", + ) + """ + _response = self._raw_client.get_runs( + dataset_name, page=page, limit=limit, request_options=request_options + ) + return _response.data + + +class AsyncDatasetsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawDatasetsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawDatasetsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawDatasetsClient + """ + return self._raw_client + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedDatasets: + """ + Get all datasets + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedDatasets + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.datasets.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def get( + self, + dataset_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> Dataset: + """ + Get a dataset + + Parameters + ---------- + dataset_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Dataset + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.datasets.get( + dataset_name="datasetName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + dataset_name, request_options=request_options + ) + return _response.data + + async def create( + self, + *, + name: str, + description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + input_schema: typing.Optional[typing.Any] = OMIT, + expected_output_schema: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Dataset: + """ + Create a dataset + + Parameters + ---------- + name : str + + description : typing.Optional[str] + + metadata : typing.Optional[typing.Any] + + input_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item inputs. When set, all new and existing dataset items will be validated against this schema. + + expected_output_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item expected outputs. When set, all new and existing dataset items will be validated against this schema. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Dataset + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.datasets.create( + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + name=name, + description=description, + metadata=metadata, + input_schema=input_schema, + expected_output_schema=expected_output_schema, + request_options=request_options, + ) + return _response.data + + async def get_run( + self, + dataset_name: str, + run_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DatasetRunWithItems: + """ + Get a dataset run and its items + + Parameters + ---------- + dataset_name : str + + run_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DatasetRunWithItems + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.datasets.get_run( + dataset_name="datasetName", + run_name="runName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_run( + dataset_name, run_name, request_options=request_options + ) + return _response.data + + async def delete_run( + self, + dataset_name: str, + run_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteDatasetRunResponse: + """ + Delete a dataset run and all its run items. This action is irreversible. + + Parameters + ---------- + dataset_name : str + + run_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteDatasetRunResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.datasets.delete_run( + dataset_name="datasetName", + run_name="runName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_run( + dataset_name, run_name, request_options=request_options + ) + return _response.data + + async def get_runs( + self, + dataset_name: str, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedDatasetRuns: + """ + Get dataset runs + + Parameters + ---------- + dataset_name : str + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedDatasetRuns + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.datasets.get_runs( + dataset_name="datasetName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_runs( + dataset_name, page=page, limit=limit, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/datasets/raw_client.py b/langfuse/api/datasets/raw_client.py new file mode 100644 index 000000000..306ad8f76 --- /dev/null +++ b/langfuse/api/datasets/raw_client.py @@ -0,0 +1,1368 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.dataset import Dataset +from ..commons.types.dataset_run_with_items import DatasetRunWithItems +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.delete_dataset_run_response import DeleteDatasetRunResponse +from .types.paginated_dataset_runs import PaginatedDatasetRuns +from .types.paginated_datasets import PaginatedDatasets + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawDatasetsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PaginatedDatasets]: + """ + Get all datasets + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedDatasets] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/v2/datasets", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedDatasets, + parse_obj_as( + type_=PaginatedDatasets, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, + dataset_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Dataset]: + """ + Get a dataset + + Parameters + ---------- + dataset_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Dataset] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/v2/datasets/{jsonable_encoder(dataset_name)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Dataset, + parse_obj_as( + type_=Dataset, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create( + self, + *, + name: str, + description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + input_schema: typing.Optional[typing.Any] = OMIT, + expected_output_schema: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Dataset]: + """ + Create a dataset + + Parameters + ---------- + name : str + + description : typing.Optional[str] + + metadata : typing.Optional[typing.Any] + + input_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item inputs. When set, all new and existing dataset items will be validated against this schema. + + expected_output_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item expected outputs. When set, all new and existing dataset items will be validated against this schema. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Dataset] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/v2/datasets", + method="POST", + json={ + "name": name, + "description": description, + "metadata": metadata, + "inputSchema": input_schema, + "expectedOutputSchema": expected_output_schema, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Dataset, + parse_obj_as( + type_=Dataset, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_run( + self, + dataset_name: str, + run_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DatasetRunWithItems]: + """ + Get a dataset run and its items + + Parameters + ---------- + dataset_name : str + + run_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DatasetRunWithItems] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DatasetRunWithItems, + parse_obj_as( + type_=DatasetRunWithItems, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_run( + self, + dataset_name: str, + run_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DeleteDatasetRunResponse]: + """ + Delete a dataset run and all its run items. This action is irreversible. + + Parameters + ---------- + dataset_name : str + + run_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteDatasetRunResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteDatasetRunResponse, + parse_obj_as( + type_=DeleteDatasetRunResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_runs( + self, + dataset_name: str, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PaginatedDatasetRuns]: + """ + Get dataset runs + + Parameters + ---------- + dataset_name : str + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedDatasetRuns] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedDatasetRuns, + parse_obj_as( + type_=PaginatedDatasetRuns, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawDatasetsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PaginatedDatasets]: + """ + Get all datasets + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedDatasets] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/v2/datasets", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedDatasets, + parse_obj_as( + type_=PaginatedDatasets, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, + dataset_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Dataset]: + """ + Get a dataset + + Parameters + ---------- + dataset_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Dataset] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/v2/datasets/{jsonable_encoder(dataset_name)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Dataset, + parse_obj_as( + type_=Dataset, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create( + self, + *, + name: str, + description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + input_schema: typing.Optional[typing.Any] = OMIT, + expected_output_schema: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Dataset]: + """ + Create a dataset + + Parameters + ---------- + name : str + + description : typing.Optional[str] + + metadata : typing.Optional[typing.Any] + + input_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item inputs. When set, all new and existing dataset items will be validated against this schema. + + expected_output_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item expected outputs. When set, all new and existing dataset items will be validated against this schema. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Dataset] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/v2/datasets", + method="POST", + json={ + "name": name, + "description": description, + "metadata": metadata, + "inputSchema": input_schema, + "expectedOutputSchema": expected_output_schema, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Dataset, + parse_obj_as( + type_=Dataset, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_run( + self, + dataset_name: str, + run_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DatasetRunWithItems]: + """ + Get a dataset run and its items + + Parameters + ---------- + dataset_name : str + + run_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DatasetRunWithItems] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DatasetRunWithItems, + parse_obj_as( + type_=DatasetRunWithItems, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_run( + self, + dataset_name: str, + run_name: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DeleteDatasetRunResponse]: + """ + Delete a dataset run and all its run items. This action is irreversible. + + Parameters + ---------- + dataset_name : str + + run_name : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteDatasetRunResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteDatasetRunResponse, + parse_obj_as( + type_=DeleteDatasetRunResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_runs( + self, + dataset_name: str, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PaginatedDatasetRuns]: + """ + Get dataset runs + + Parameters + ---------- + dataset_name : str + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedDatasetRuns] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedDatasetRuns, + parse_obj_as( + type_=PaginatedDatasetRuns, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/datasets/types/__init__.py b/langfuse/api/datasets/types/__init__.py new file mode 100644 index 000000000..60c58934a --- /dev/null +++ b/langfuse/api/datasets/types/__init__.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_dataset_request import CreateDatasetRequest + from .delete_dataset_run_response import DeleteDatasetRunResponse + from .paginated_dataset_runs import PaginatedDatasetRuns + from .paginated_datasets import PaginatedDatasets +_dynamic_imports: typing.Dict[str, str] = { + "CreateDatasetRequest": ".create_dataset_request", + "DeleteDatasetRunResponse": ".delete_dataset_run_response", + "PaginatedDatasetRuns": ".paginated_dataset_runs", + "PaginatedDatasets": ".paginated_datasets", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CreateDatasetRequest", + "DeleteDatasetRunResponse", + "PaginatedDatasetRuns", + "PaginatedDatasets", +] diff --git a/langfuse/api/datasets/types/create_dataset_request.py b/langfuse/api/datasets/types/create_dataset_request.py new file mode 100644 index 000000000..9d9b01089 --- /dev/null +++ b/langfuse/api/datasets/types/create_dataset_request.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class CreateDatasetRequest(UniversalBaseModel): + name: str + description: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None + input_schema: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="inputSchema") + ] = pydantic.Field(default=None) + """ + JSON Schema for validating dataset item inputs. When set, all new and existing dataset items will be validated against this schema. + """ + + expected_output_schema: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="expectedOutputSchema") + ] = pydantic.Field(default=None) + """ + JSON Schema for validating dataset item expected outputs. When set, all new and existing dataset items will be validated against this schema. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/datasets/types/delete_dataset_run_response.py b/langfuse/api/datasets/types/delete_dataset_run_response.py new file mode 100644 index 000000000..024433ef6 --- /dev/null +++ b/langfuse/api/datasets/types/delete_dataset_run_response.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class DeleteDatasetRunResponse(UniversalBaseModel): + message: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/datasets/types/paginated_dataset_runs.py b/langfuse/api/datasets/types/paginated_dataset_runs.py new file mode 100644 index 000000000..85cd54c55 --- /dev/null +++ b/langfuse/api/datasets/types/paginated_dataset_runs.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.dataset_run import DatasetRun +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse + + +class PaginatedDatasetRuns(UniversalBaseModel): + data: typing.List[DatasetRun] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/datasets/types/paginated_datasets.py b/langfuse/api/datasets/types/paginated_datasets.py new file mode 100644 index 000000000..a2e83edf1 --- /dev/null +++ b/langfuse/api/datasets/types/paginated_datasets.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.dataset import Dataset +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse + + +class PaginatedDatasets(UniversalBaseModel): + data: typing.List[Dataset] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/health/__init__.py b/langfuse/api/health/__init__.py new file mode 100644 index 000000000..2a101c5f7 --- /dev/null +++ b/langfuse/api/health/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import HealthResponse + from .errors import ServiceUnavailableError +_dynamic_imports: typing.Dict[str, str] = { + "HealthResponse": ".types", + "ServiceUnavailableError": ".errors", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["HealthResponse", "ServiceUnavailableError"] diff --git a/langfuse/api/health/client.py b/langfuse/api/health/client.py new file mode 100644 index 000000000..7406fef6f --- /dev/null +++ b/langfuse/api/health/client.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawHealthClient, RawHealthClient +from .types.health_response import HealthResponse + + +class HealthClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawHealthClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawHealthClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawHealthClient + """ + return self._raw_client + + def health( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HealthResponse: + """ + Check health of API and database + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HealthResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.health.health() + """ + _response = self._raw_client.health(request_options=request_options) + return _response.data + + +class AsyncHealthClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawHealthClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawHealthClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawHealthClient + """ + return self._raw_client + + async def health( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HealthResponse: + """ + Check health of API and database + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HealthResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.health.health() + + + asyncio.run(main()) + """ + _response = await self._raw_client.health(request_options=request_options) + return _response.data diff --git a/langfuse/api/health/errors/__init__.py b/langfuse/api/health/errors/__init__.py new file mode 100644 index 000000000..3567f86b3 --- /dev/null +++ b/langfuse/api/health/errors/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .service_unavailable_error import ServiceUnavailableError +_dynamic_imports: typing.Dict[str, str] = { + "ServiceUnavailableError": ".service_unavailable_error" +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["ServiceUnavailableError"] diff --git a/langfuse/api/health/errors/service_unavailable_error.py b/langfuse/api/health/errors/service_unavailable_error.py new file mode 100644 index 000000000..68d5d836e --- /dev/null +++ b/langfuse/api/health/errors/service_unavailable_error.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class ServiceUnavailableError(ApiError): + def __init__(self, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__( + status_code=503, + headers=headers, + ) diff --git a/langfuse/api/health/raw_client.py b/langfuse/api/health/raw_client.py new file mode 100644 index 000000000..afeef1a96 --- /dev/null +++ b/langfuse/api/health/raw_client.py @@ -0,0 +1,227 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .errors.service_unavailable_error import ServiceUnavailableError +from .types.health_response import HealthResponse + + +class RawHealthClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def health( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[HealthResponse]: + """ + Check health of API and database + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[HealthResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/health", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + HealthResponse, + parse_obj_as( + type_=HealthResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 503: + raise ServiceUnavailableError(headers=dict(_response.headers)) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawHealthClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def health( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[HealthResponse]: + """ + Check health of API and database + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[HealthResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/health", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + HealthResponse, + parse_obj_as( + type_=HealthResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 503: + raise ServiceUnavailableError(headers=dict(_response.headers)) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/health/types/__init__.py b/langfuse/api/health/types/__init__.py new file mode 100644 index 000000000..d4bec6804 --- /dev/null +++ b/langfuse/api/health/types/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .health_response import HealthResponse +_dynamic_imports: typing.Dict[str, str] = {"HealthResponse": ".health_response"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["HealthResponse"] diff --git a/langfuse/api/health/types/health_response.py b/langfuse/api/health/types/health_response.py new file mode 100644 index 000000000..a1c7a4bea --- /dev/null +++ b/langfuse/api/health/types/health_response.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class HealthResponse(UniversalBaseModel): + """ + Examples + -------- + from langfuse.health import HealthResponse + + HealthResponse( + version="1.25.0", + status="OK", + ) + """ + + version: str = pydantic.Field() + """ + Langfuse server version + """ + + status: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/__init__.py b/langfuse/api/ingestion/__init__.py new file mode 100644 index 000000000..5cd4ba3bd --- /dev/null +++ b/langfuse/api/ingestion/__init__.py @@ -0,0 +1,169 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + BaseEvent, + CreateEventBody, + CreateEventEvent, + CreateGenerationBody, + CreateGenerationEvent, + CreateObservationEvent, + CreateSpanBody, + CreateSpanEvent, + IngestionError, + IngestionEvent, + IngestionEvent_EventCreate, + IngestionEvent_GenerationCreate, + IngestionEvent_GenerationUpdate, + IngestionEvent_ObservationCreate, + IngestionEvent_ObservationUpdate, + IngestionEvent_ScoreCreate, + IngestionEvent_SdkLog, + IngestionEvent_SpanCreate, + IngestionEvent_SpanUpdate, + IngestionEvent_TraceCreate, + IngestionResponse, + IngestionSuccess, + IngestionUsage, + ObservationBody, + ObservationType, + OpenAiCompletionUsageSchema, + OpenAiResponseUsageSchema, + OpenAiUsage, + OptionalObservationBody, + ScoreBody, + ScoreEvent, + SdkLogBody, + SdkLogEvent, + TraceBody, + TraceEvent, + UpdateEventBody, + UpdateGenerationBody, + UpdateGenerationEvent, + UpdateObservationEvent, + UpdateSpanBody, + UpdateSpanEvent, + UsageDetails, + ) +_dynamic_imports: typing.Dict[str, str] = { + "BaseEvent": ".types", + "CreateEventBody": ".types", + "CreateEventEvent": ".types", + "CreateGenerationBody": ".types", + "CreateGenerationEvent": ".types", + "CreateObservationEvent": ".types", + "CreateSpanBody": ".types", + "CreateSpanEvent": ".types", + "IngestionError": ".types", + "IngestionEvent": ".types", + "IngestionEvent_EventCreate": ".types", + "IngestionEvent_GenerationCreate": ".types", + "IngestionEvent_GenerationUpdate": ".types", + "IngestionEvent_ObservationCreate": ".types", + "IngestionEvent_ObservationUpdate": ".types", + "IngestionEvent_ScoreCreate": ".types", + "IngestionEvent_SdkLog": ".types", + "IngestionEvent_SpanCreate": ".types", + "IngestionEvent_SpanUpdate": ".types", + "IngestionEvent_TraceCreate": ".types", + "IngestionResponse": ".types", + "IngestionSuccess": ".types", + "IngestionUsage": ".types", + "ObservationBody": ".types", + "ObservationType": ".types", + "OpenAiCompletionUsageSchema": ".types", + "OpenAiResponseUsageSchema": ".types", + "OpenAiUsage": ".types", + "OptionalObservationBody": ".types", + "ScoreBody": ".types", + "ScoreEvent": ".types", + "SdkLogBody": ".types", + "SdkLogEvent": ".types", + "TraceBody": ".types", + "TraceEvent": ".types", + "UpdateEventBody": ".types", + "UpdateGenerationBody": ".types", + "UpdateGenerationEvent": ".types", + "UpdateObservationEvent": ".types", + "UpdateSpanBody": ".types", + "UpdateSpanEvent": ".types", + "UsageDetails": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BaseEvent", + "CreateEventBody", + "CreateEventEvent", + "CreateGenerationBody", + "CreateGenerationEvent", + "CreateObservationEvent", + "CreateSpanBody", + "CreateSpanEvent", + "IngestionError", + "IngestionEvent", + "IngestionEvent_EventCreate", + "IngestionEvent_GenerationCreate", + "IngestionEvent_GenerationUpdate", + "IngestionEvent_ObservationCreate", + "IngestionEvent_ObservationUpdate", + "IngestionEvent_ScoreCreate", + "IngestionEvent_SdkLog", + "IngestionEvent_SpanCreate", + "IngestionEvent_SpanUpdate", + "IngestionEvent_TraceCreate", + "IngestionResponse", + "IngestionSuccess", + "IngestionUsage", + "ObservationBody", + "ObservationType", + "OpenAiCompletionUsageSchema", + "OpenAiResponseUsageSchema", + "OpenAiUsage", + "OptionalObservationBody", + "ScoreBody", + "ScoreEvent", + "SdkLogBody", + "SdkLogEvent", + "TraceBody", + "TraceEvent", + "UpdateEventBody", + "UpdateGenerationBody", + "UpdateGenerationEvent", + "UpdateObservationEvent", + "UpdateSpanBody", + "UpdateSpanEvent", + "UsageDetails", +] diff --git a/langfuse/api/resources/ingestion/client.py b/langfuse/api/ingestion/client.py similarity index 60% rename from langfuse/api/resources/ingestion/client.py rename to langfuse/api/ingestion/client.py index 9d6784856..af1da8396 100644 --- a/langfuse/api/resources/ingestion/client.py +++ b/langfuse/api/ingestion/client.py @@ -1,17 +1,10 @@ # This file was auto-generated by Fern from our API Definition. import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawIngestionClient, RawIngestionClient from .types.ingestion_event import IngestionEvent from .types.ingestion_response import IngestionResponse @@ -21,7 +14,18 @@ class IngestionClient: def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper + self._raw_client = RawIngestionClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawIngestionClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawIngestionClient + """ + return self._raw_client def batch( self, @@ -31,8 +35,9 @@ def batch( request_options: typing.Optional[RequestOptions] = None, ) -> IngestionResponse: """ - Batched ingestion for Langfuse Tracing. - If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement. + **Legacy endpoint for batch ingestion for Langfuse Observability.** + + -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). Learn more: https://langfuse.com/integrations/native/opentelemetry Within each batch, there can be multiple events. Each event has a type, an id, a timestamp, metadata and a body. @@ -42,7 +47,7 @@ def batch( I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. Notes: - - Introduction to data model: https://langfuse.com/docs/tracing-data-model + - Introduction to data model: https://langfuse.com/docs/observability/data-model - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. @@ -65,10 +70,10 @@ def batch( -------- import datetime - from langfuse import IngestionEvent_TraceCreate, TraceBody - from langfuse.client import FernLangfuse + from langfuse import LangfuseAPI + from langfuse.ingestion import IngestionEvent_TraceCreate, TraceBody - client = FernLangfuse( + client = LangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -102,43 +107,26 @@ def batch( ], ) """ - _response = self._client_wrapper.httpx_client.request( - "api/public/ingestion", - method="POST", - json={"batch": batch, "metadata": metadata}, - request_options=request_options, - omit=OMIT, + _response = self._raw_client.batch( + batch=batch, metadata=metadata, request_options=request_options ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(IngestionResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) + return _response.data class AsyncIngestionClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper + self._raw_client = AsyncRawIngestionClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawIngestionClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawIngestionClient + """ + return self._raw_client async def batch( self, @@ -148,8 +136,9 @@ async def batch( request_options: typing.Optional[RequestOptions] = None, ) -> IngestionResponse: """ - Batched ingestion for Langfuse Tracing. - If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement. + **Legacy endpoint for batch ingestion for Langfuse Observability.** + + -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). Learn more: https://langfuse.com/integrations/native/opentelemetry Within each batch, there can be multiple events. Each event has a type, an id, a timestamp, metadata and a body. @@ -159,7 +148,7 @@ async def batch( I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. Notes: - - Introduction to data model: https://langfuse.com/docs/tracing-data-model + - Introduction to data model: https://langfuse.com/docs/observability/data-model - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. @@ -183,10 +172,10 @@ async def batch( import asyncio import datetime - from langfuse import IngestionEvent_TraceCreate, TraceBody - from langfuse.client import AsyncFernLangfuse + from langfuse import AsyncLangfuseAPI + from langfuse.ingestion import IngestionEvent_TraceCreate, TraceBody - client = AsyncFernLangfuse( + client = AsyncLangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -226,35 +215,7 @@ async def main() -> None: asyncio.run(main()) """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/ingestion", - method="POST", - json={"batch": batch, "metadata": metadata}, - request_options=request_options, - omit=OMIT, + _response = await self._raw_client.batch( + batch=batch, metadata=metadata, request_options=request_options ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(IngestionResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) + return _response.data diff --git a/langfuse/api/ingestion/raw_client.py b/langfuse/api/ingestion/raw_client.py new file mode 100644 index 000000000..cb60a5fba --- /dev/null +++ b/langfuse/api/ingestion/raw_client.py @@ -0,0 +1,293 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from .types.ingestion_event import IngestionEvent +from .types.ingestion_response import IngestionResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawIngestionClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def batch( + self, + *, + batch: typing.Sequence[IngestionEvent], + metadata: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[IngestionResponse]: + """ + **Legacy endpoint for batch ingestion for Langfuse Observability.** + + -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). Learn more: https://langfuse.com/integrations/native/opentelemetry + + Within each batch, there can be multiple events. + Each event has a type, an id, a timestamp, metadata and a body. + Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace. + We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request. + The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App. + I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. + + Notes: + - Introduction to data model: https://langfuse.com/docs/observability/data-model + - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. + - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. + + Parameters + ---------- + batch : typing.Sequence[IngestionEvent] + Batch of tracing events to be ingested. Discriminated by attribute `type`. + + metadata : typing.Optional[typing.Any] + Optional. Metadata field used by the Langfuse SDKs for debugging. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[IngestionResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/ingestion", + method="POST", + json={ + "batch": convert_and_respect_annotation_metadata( + object_=batch, + annotation=typing.Sequence[IngestionEvent], + direction="write", + ), + "metadata": metadata, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + IngestionResponse, + parse_obj_as( + type_=IngestionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawIngestionClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def batch( + self, + *, + batch: typing.Sequence[IngestionEvent], + metadata: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[IngestionResponse]: + """ + **Legacy endpoint for batch ingestion for Langfuse Observability.** + + -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). Learn more: https://langfuse.com/integrations/native/opentelemetry + + Within each batch, there can be multiple events. + Each event has a type, an id, a timestamp, metadata and a body. + Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace. + We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request. + The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App. + I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. + + Notes: + - Introduction to data model: https://langfuse.com/docs/observability/data-model + - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. + - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. + + Parameters + ---------- + batch : typing.Sequence[IngestionEvent] + Batch of tracing events to be ingested. Discriminated by attribute `type`. + + metadata : typing.Optional[typing.Any] + Optional. Metadata field used by the Langfuse SDKs for debugging. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[IngestionResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/ingestion", + method="POST", + json={ + "batch": convert_and_respect_annotation_metadata( + object_=batch, + annotation=typing.Sequence[IngestionEvent], + direction="write", + ), + "metadata": metadata, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + IngestionResponse, + parse_obj_as( + type_=IngestionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/ingestion/types/__init__.py b/langfuse/api/ingestion/types/__init__.py new file mode 100644 index 000000000..4addfd9c7 --- /dev/null +++ b/langfuse/api/ingestion/types/__init__.py @@ -0,0 +1,169 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .base_event import BaseEvent + from .create_event_body import CreateEventBody + from .create_event_event import CreateEventEvent + from .create_generation_body import CreateGenerationBody + from .create_generation_event import CreateGenerationEvent + from .create_observation_event import CreateObservationEvent + from .create_span_body import CreateSpanBody + from .create_span_event import CreateSpanEvent + from .ingestion_error import IngestionError + from .ingestion_event import ( + IngestionEvent, + IngestionEvent_EventCreate, + IngestionEvent_GenerationCreate, + IngestionEvent_GenerationUpdate, + IngestionEvent_ObservationCreate, + IngestionEvent_ObservationUpdate, + IngestionEvent_ScoreCreate, + IngestionEvent_SdkLog, + IngestionEvent_SpanCreate, + IngestionEvent_SpanUpdate, + IngestionEvent_TraceCreate, + ) + from .ingestion_response import IngestionResponse + from .ingestion_success import IngestionSuccess + from .ingestion_usage import IngestionUsage + from .observation_body import ObservationBody + from .observation_type import ObservationType + from .open_ai_completion_usage_schema import OpenAiCompletionUsageSchema + from .open_ai_response_usage_schema import OpenAiResponseUsageSchema + from .open_ai_usage import OpenAiUsage + from .optional_observation_body import OptionalObservationBody + from .score_body import ScoreBody + from .score_event import ScoreEvent + from .sdk_log_body import SdkLogBody + from .sdk_log_event import SdkLogEvent + from .trace_body import TraceBody + from .trace_event import TraceEvent + from .update_event_body import UpdateEventBody + from .update_generation_body import UpdateGenerationBody + from .update_generation_event import UpdateGenerationEvent + from .update_observation_event import UpdateObservationEvent + from .update_span_body import UpdateSpanBody + from .update_span_event import UpdateSpanEvent + from .usage_details import UsageDetails +_dynamic_imports: typing.Dict[str, str] = { + "BaseEvent": ".base_event", + "CreateEventBody": ".create_event_body", + "CreateEventEvent": ".create_event_event", + "CreateGenerationBody": ".create_generation_body", + "CreateGenerationEvent": ".create_generation_event", + "CreateObservationEvent": ".create_observation_event", + "CreateSpanBody": ".create_span_body", + "CreateSpanEvent": ".create_span_event", + "IngestionError": ".ingestion_error", + "IngestionEvent": ".ingestion_event", + "IngestionEvent_EventCreate": ".ingestion_event", + "IngestionEvent_GenerationCreate": ".ingestion_event", + "IngestionEvent_GenerationUpdate": ".ingestion_event", + "IngestionEvent_ObservationCreate": ".ingestion_event", + "IngestionEvent_ObservationUpdate": ".ingestion_event", + "IngestionEvent_ScoreCreate": ".ingestion_event", + "IngestionEvent_SdkLog": ".ingestion_event", + "IngestionEvent_SpanCreate": ".ingestion_event", + "IngestionEvent_SpanUpdate": ".ingestion_event", + "IngestionEvent_TraceCreate": ".ingestion_event", + "IngestionResponse": ".ingestion_response", + "IngestionSuccess": ".ingestion_success", + "IngestionUsage": ".ingestion_usage", + "ObservationBody": ".observation_body", + "ObservationType": ".observation_type", + "OpenAiCompletionUsageSchema": ".open_ai_completion_usage_schema", + "OpenAiResponseUsageSchema": ".open_ai_response_usage_schema", + "OpenAiUsage": ".open_ai_usage", + "OptionalObservationBody": ".optional_observation_body", + "ScoreBody": ".score_body", + "ScoreEvent": ".score_event", + "SdkLogBody": ".sdk_log_body", + "SdkLogEvent": ".sdk_log_event", + "TraceBody": ".trace_body", + "TraceEvent": ".trace_event", + "UpdateEventBody": ".update_event_body", + "UpdateGenerationBody": ".update_generation_body", + "UpdateGenerationEvent": ".update_generation_event", + "UpdateObservationEvent": ".update_observation_event", + "UpdateSpanBody": ".update_span_body", + "UpdateSpanEvent": ".update_span_event", + "UsageDetails": ".usage_details", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BaseEvent", + "CreateEventBody", + "CreateEventEvent", + "CreateGenerationBody", + "CreateGenerationEvent", + "CreateObservationEvent", + "CreateSpanBody", + "CreateSpanEvent", + "IngestionError", + "IngestionEvent", + "IngestionEvent_EventCreate", + "IngestionEvent_GenerationCreate", + "IngestionEvent_GenerationUpdate", + "IngestionEvent_ObservationCreate", + "IngestionEvent_ObservationUpdate", + "IngestionEvent_ScoreCreate", + "IngestionEvent_SdkLog", + "IngestionEvent_SpanCreate", + "IngestionEvent_SpanUpdate", + "IngestionEvent_TraceCreate", + "IngestionResponse", + "IngestionSuccess", + "IngestionUsage", + "ObservationBody", + "ObservationType", + "OpenAiCompletionUsageSchema", + "OpenAiResponseUsageSchema", + "OpenAiUsage", + "OptionalObservationBody", + "ScoreBody", + "ScoreEvent", + "SdkLogBody", + "SdkLogEvent", + "TraceBody", + "TraceEvent", + "UpdateEventBody", + "UpdateGenerationBody", + "UpdateGenerationEvent", + "UpdateObservationEvent", + "UpdateSpanBody", + "UpdateSpanEvent", + "UsageDetails", +] diff --git a/langfuse/api/ingestion/types/base_event.py b/langfuse/api/ingestion/types/base_event.py new file mode 100644 index 000000000..70c6bcfa4 --- /dev/null +++ b/langfuse/api/ingestion/types/base_event.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class BaseEvent(UniversalBaseModel): + id: str = pydantic.Field() + """ + UUID v4 that identifies the event + """ + + timestamp: str = pydantic.Field() + """ + Datetime (ISO 8601) of event creation in client. Should be as close to actual event creation in client as possible, this timestamp will be used for ordering of events in future release. Resolution: milliseconds (required), microseconds (optimal). + """ + + metadata: typing.Optional[typing.Any] = pydantic.Field(default=None) + """ + Optional. Metadata field used by the Langfuse SDKs for debugging. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/create_event_body.py b/langfuse/api/ingestion/types/create_event_body.py new file mode 100644 index 000000000..0473d8a0d --- /dev/null +++ b/langfuse/api/ingestion/types/create_event_body.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .optional_observation_body import OptionalObservationBody + + +class CreateEventBody(OptionalObservationBody): + id: typing.Optional[str] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/create_event_event.py b/langfuse/api/ingestion/types/create_event_event.py new file mode 100644 index 000000000..e0cc820e1 --- /dev/null +++ b/langfuse/api/ingestion/types/create_event_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .create_event_body import CreateEventBody + + +class CreateEventEvent(BaseEvent): + body: CreateEventBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/create_generation_body.py b/langfuse/api/ingestion/types/create_generation_body.py new file mode 100644 index 000000000..72cb57116 --- /dev/null +++ b/langfuse/api/ingestion/types/create_generation_body.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...commons.types.map_value import MapValue +from ...core.serialization import FieldMetadata +from .create_span_body import CreateSpanBody +from .ingestion_usage import IngestionUsage +from .usage_details import UsageDetails + + +class CreateGenerationBody(CreateSpanBody): + completion_start_time: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="completionStartTime") + ] = None + model: typing.Optional[str] = None + model_parameters: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, MapValue]], + FieldMetadata(alias="modelParameters"), + ] = None + usage: typing.Optional[IngestionUsage] = None + usage_details: typing_extensions.Annotated[ + typing.Optional[UsageDetails], FieldMetadata(alias="usageDetails") + ] = None + cost_details: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, float]], FieldMetadata(alias="costDetails") + ] = None + prompt_name: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="promptName") + ] = None + prompt_version: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="promptVersion") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/create_generation_event.py b/langfuse/api/ingestion/types/create_generation_event.py new file mode 100644 index 000000000..d62d6cc41 --- /dev/null +++ b/langfuse/api/ingestion/types/create_generation_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .create_generation_body import CreateGenerationBody + + +class CreateGenerationEvent(BaseEvent): + body: CreateGenerationBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/create_observation_event.py b/langfuse/api/ingestion/types/create_observation_event.py new file mode 100644 index 000000000..06d927f36 --- /dev/null +++ b/langfuse/api/ingestion/types/create_observation_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .observation_body import ObservationBody + + +class CreateObservationEvent(BaseEvent): + body: ObservationBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/create_span_body.py b/langfuse/api/ingestion/types/create_span_body.py new file mode 100644 index 000000000..7a47d9748 --- /dev/null +++ b/langfuse/api/ingestion/types/create_span_body.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .create_event_body import CreateEventBody + + +class CreateSpanBody(CreateEventBody): + end_time: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="endTime") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/create_span_event.py b/langfuse/api/ingestion/types/create_span_event.py new file mode 100644 index 000000000..6e60cf1fe --- /dev/null +++ b/langfuse/api/ingestion/types/create_span_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .create_span_body import CreateSpanBody + + +class CreateSpanEvent(BaseEvent): + body: CreateSpanBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/ingestion_error.py b/langfuse/api/ingestion/types/ingestion_error.py new file mode 100644 index 000000000..2391d95eb --- /dev/null +++ b/langfuse/api/ingestion/types/ingestion_error.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class IngestionError(UniversalBaseModel): + id: str + status: int + message: typing.Optional[str] = None + error: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/ingestion_event.py b/langfuse/api/ingestion/types/ingestion_event.py new file mode 100644 index 000000000..03202e635 --- /dev/null +++ b/langfuse/api/ingestion/types/ingestion_event.py @@ -0,0 +1,155 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from .create_event_body import CreateEventBody +from .create_generation_body import CreateGenerationBody +from .create_span_body import CreateSpanBody +from .observation_body import ObservationBody +from .score_body import ScoreBody +from .sdk_log_body import SdkLogBody +from .trace_body import TraceBody +from .update_generation_body import UpdateGenerationBody +from .update_span_body import UpdateSpanBody + + +class IngestionEvent_TraceCreate(UniversalBaseModel): + type: typing.Literal["trace-create"] = "trace-create" + body: TraceBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_ScoreCreate(UniversalBaseModel): + type: typing.Literal["score-create"] = "score-create" + body: ScoreBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_SpanCreate(UniversalBaseModel): + type: typing.Literal["span-create"] = "span-create" + body: CreateSpanBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_SpanUpdate(UniversalBaseModel): + type: typing.Literal["span-update"] = "span-update" + body: UpdateSpanBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_GenerationCreate(UniversalBaseModel): + type: typing.Literal["generation-create"] = "generation-create" + body: CreateGenerationBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_GenerationUpdate(UniversalBaseModel): + type: typing.Literal["generation-update"] = "generation-update" + body: UpdateGenerationBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_EventCreate(UniversalBaseModel): + type: typing.Literal["event-create"] = "event-create" + body: CreateEventBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_SdkLog(UniversalBaseModel): + type: typing.Literal["sdk-log"] = "sdk-log" + body: SdkLogBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_ObservationCreate(UniversalBaseModel): + type: typing.Literal["observation-create"] = "observation-create" + body: ObservationBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class IngestionEvent_ObservationUpdate(UniversalBaseModel): + type: typing.Literal["observation-update"] = "observation-update" + body: ObservationBody + id: str + timestamp: str + metadata: typing.Optional[typing.Any] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +IngestionEvent = typing_extensions.Annotated[ + typing.Union[ + IngestionEvent_TraceCreate, + IngestionEvent_ScoreCreate, + IngestionEvent_SpanCreate, + IngestionEvent_SpanUpdate, + IngestionEvent_GenerationCreate, + IngestionEvent_GenerationUpdate, + IngestionEvent_EventCreate, + IngestionEvent_SdkLog, + IngestionEvent_ObservationCreate, + IngestionEvent_ObservationUpdate, + ], + pydantic.Field(discriminator="type"), +] diff --git a/langfuse/api/ingestion/types/ingestion_response.py b/langfuse/api/ingestion/types/ingestion_response.py new file mode 100644 index 000000000..b9781fab5 --- /dev/null +++ b/langfuse/api/ingestion/types/ingestion_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .ingestion_error import IngestionError +from .ingestion_success import IngestionSuccess + + +class IngestionResponse(UniversalBaseModel): + successes: typing.List[IngestionSuccess] + errors: typing.List[IngestionError] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/ingestion_success.py b/langfuse/api/ingestion/types/ingestion_success.py new file mode 100644 index 000000000..e7894797a --- /dev/null +++ b/langfuse/api/ingestion/types/ingestion_success.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class IngestionSuccess(UniversalBaseModel): + id: str + status: int + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/resources/ingestion/types/ingestion_usage.py b/langfuse/api/ingestion/types/ingestion_usage.py similarity index 100% rename from langfuse/api/resources/ingestion/types/ingestion_usage.py rename to langfuse/api/ingestion/types/ingestion_usage.py diff --git a/langfuse/api/ingestion/types/observation_body.py b/langfuse/api/ingestion/types/observation_body.py new file mode 100644 index 000000000..e989a768f --- /dev/null +++ b/langfuse/api/ingestion/types/observation_body.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...commons.types.map_value import MapValue +from ...commons.types.observation_level import ObservationLevel +from ...commons.types.usage import Usage +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .observation_type import ObservationType + + +class ObservationBody(UniversalBaseModel): + id: typing.Optional[str] = None + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + type: ObservationType + name: typing.Optional[str] = None + start_time: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="startTime") + ] = None + end_time: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="endTime") + ] = None + completion_start_time: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="completionStartTime") + ] = None + model: typing.Optional[str] = None + model_parameters: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, MapValue]], + FieldMetadata(alias="modelParameters"), + ] = None + input: typing.Optional[typing.Any] = None + version: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None + output: typing.Optional[typing.Any] = None + usage: typing.Optional[Usage] = None + level: typing.Optional[ObservationLevel] = None + status_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="statusMessage") + ] = None + parent_observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="parentObservationId") + ] = None + environment: typing.Optional[str] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/observation_type.py b/langfuse/api/ingestion/types/observation_type.py new file mode 100644 index 000000000..f769e34cf --- /dev/null +++ b/langfuse/api/ingestion/types/observation_type.py @@ -0,0 +1,54 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ObservationType(enum.StrEnum): + SPAN = "SPAN" + GENERATION = "GENERATION" + EVENT = "EVENT" + AGENT = "AGENT" + TOOL = "TOOL" + CHAIN = "CHAIN" + RETRIEVER = "RETRIEVER" + EVALUATOR = "EVALUATOR" + EMBEDDING = "EMBEDDING" + GUARDRAIL = "GUARDRAIL" + + def visit( + self, + span: typing.Callable[[], T_Result], + generation: typing.Callable[[], T_Result], + event: typing.Callable[[], T_Result], + agent: typing.Callable[[], T_Result], + tool: typing.Callable[[], T_Result], + chain: typing.Callable[[], T_Result], + retriever: typing.Callable[[], T_Result], + evaluator: typing.Callable[[], T_Result], + embedding: typing.Callable[[], T_Result], + guardrail: typing.Callable[[], T_Result], + ) -> T_Result: + if self is ObservationType.SPAN: + return span() + if self is ObservationType.GENERATION: + return generation() + if self is ObservationType.EVENT: + return event() + if self is ObservationType.AGENT: + return agent() + if self is ObservationType.TOOL: + return tool() + if self is ObservationType.CHAIN: + return chain() + if self is ObservationType.RETRIEVER: + return retriever() + if self is ObservationType.EVALUATOR: + return evaluator() + if self is ObservationType.EMBEDDING: + return embedding() + if self is ObservationType.GUARDRAIL: + return guardrail() diff --git a/langfuse/api/ingestion/types/open_ai_completion_usage_schema.py b/langfuse/api/ingestion/types/open_ai_completion_usage_schema.py new file mode 100644 index 000000000..292c51e79 --- /dev/null +++ b/langfuse/api/ingestion/types/open_ai_completion_usage_schema.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class OpenAiCompletionUsageSchema(UniversalBaseModel): + """ + OpenAI Usage schema from (Chat-)Completion APIs + """ + + prompt_tokens: int + completion_tokens: int + total_tokens: int + prompt_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = ( + None + ) + completion_tokens_details: typing.Optional[ + typing.Dict[str, typing.Optional[int]] + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/open_ai_response_usage_schema.py b/langfuse/api/ingestion/types/open_ai_response_usage_schema.py new file mode 100644 index 000000000..93cbc7dba --- /dev/null +++ b/langfuse/api/ingestion/types/open_ai_response_usage_schema.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class OpenAiResponseUsageSchema(UniversalBaseModel): + """ + OpenAI Usage schema from Response API + """ + + input_tokens: int + output_tokens: int + total_tokens: int + input_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = None + output_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = ( + None + ) + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/open_ai_usage.py b/langfuse/api/ingestion/types/open_ai_usage.py new file mode 100644 index 000000000..7c1ab3160 --- /dev/null +++ b/langfuse/api/ingestion/types/open_ai_usage.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class OpenAiUsage(UniversalBaseModel): + """ + Usage interface of OpenAI for improved compatibility. + """ + + prompt_tokens: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="promptTokens") + ] = None + completion_tokens: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="completionTokens") + ] = None + total_tokens: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="totalTokens") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/optional_observation_body.py b/langfuse/api/ingestion/types/optional_observation_body.py new file mode 100644 index 000000000..f2aaf9b6d --- /dev/null +++ b/langfuse/api/ingestion/types/optional_observation_body.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...commons.types.observation_level import ObservationLevel +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class OptionalObservationBody(UniversalBaseModel): + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + name: typing.Optional[str] = None + start_time: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="startTime") + ] = None + metadata: typing.Optional[typing.Any] = None + input: typing.Optional[typing.Any] = None + output: typing.Optional[typing.Any] = None + level: typing.Optional[ObservationLevel] = None + status_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="statusMessage") + ] = None + parent_observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="parentObservationId") + ] = None + version: typing.Optional[str] = None + environment: typing.Optional[str] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/score_body.py b/langfuse/api/ingestion/types/score_body.py new file mode 100644 index 000000000..d9a32b6a7 --- /dev/null +++ b/langfuse/api/ingestion/types/score_body.py @@ -0,0 +1,75 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...commons.types.create_score_value import CreateScoreValue +from ...commons.types.score_data_type import ScoreDataType +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class ScoreBody(UniversalBaseModel): + """ + Examples + -------- + from langfuse.ingestion import ScoreBody + + ScoreBody( + name="novelty", + value=0.9, + trace_id="cdef-1234-5678-90ab", + ) + """ + + id: typing.Optional[str] = None + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str = pydantic.Field() + """ + The name of the score. Always overrides "output" for correction scores. + """ + + environment: typing.Optional[str] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = pydantic.Field(default=None) + """ + The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + """ + + value: CreateScoreValue = pydantic.Field() + """ + The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. + """ + + comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None + data_type: typing_extensions.Annotated[ + typing.Optional[ScoreDataType], FieldMetadata(alias="dataType") + ] = pydantic.Field(default=None) + """ + When set, must match the score value's type. If not set, will be inferred from the score value or config + """ + + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = pydantic.Field(default=None) + """ + Reference a score config on a score. When set, the score name must equal the config name and scores must comply with the config's range and data type. For categorical scores, the value must map to a config category. Numeric scores might be constrained by the score config's max and min values + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/score_event.py b/langfuse/api/ingestion/types/score_event.py new file mode 100644 index 000000000..d0d470899 --- /dev/null +++ b/langfuse/api/ingestion/types/score_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .score_body import ScoreBody + + +class ScoreEvent(BaseEvent): + body: ScoreBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/sdk_log_body.py b/langfuse/api/ingestion/types/sdk_log_body.py new file mode 100644 index 000000000..d5b46f118 --- /dev/null +++ b/langfuse/api/ingestion/types/sdk_log_body.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class SdkLogBody(UniversalBaseModel): + log: typing.Any + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/sdk_log_event.py b/langfuse/api/ingestion/types/sdk_log_event.py new file mode 100644 index 000000000..ca303af55 --- /dev/null +++ b/langfuse/api/ingestion/types/sdk_log_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .sdk_log_body import SdkLogBody + + +class SdkLogEvent(BaseEvent): + body: SdkLogBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/trace_body.py b/langfuse/api/ingestion/types/trace_body.py new file mode 100644 index 000000000..7fb2842a0 --- /dev/null +++ b/langfuse/api/ingestion/types/trace_body.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class TraceBody(UniversalBaseModel): + id: typing.Optional[str] = None + timestamp: typing.Optional[dt.datetime] = None + name: typing.Optional[str] = None + user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="userId") + ] = None + input: typing.Optional[typing.Any] = None + output: typing.Optional[typing.Any] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + release: typing.Optional[str] = None + version: typing.Optional[str] = None + metadata: typing.Optional[typing.Any] = None + tags: typing.Optional[typing.List[str]] = None + environment: typing.Optional[str] = None + public: typing.Optional[bool] = pydantic.Field(default=None) + """ + Make trace publicly accessible via url + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/trace_event.py b/langfuse/api/ingestion/types/trace_event.py new file mode 100644 index 000000000..54127597a --- /dev/null +++ b/langfuse/api/ingestion/types/trace_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .trace_body import TraceBody + + +class TraceEvent(BaseEvent): + body: TraceBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/update_event_body.py b/langfuse/api/ingestion/types/update_event_body.py new file mode 100644 index 000000000..055f66f08 --- /dev/null +++ b/langfuse/api/ingestion/types/update_event_body.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .optional_observation_body import OptionalObservationBody + + +class UpdateEventBody(OptionalObservationBody): + id: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/update_generation_body.py b/langfuse/api/ingestion/types/update_generation_body.py new file mode 100644 index 000000000..1d453e759 --- /dev/null +++ b/langfuse/api/ingestion/types/update_generation_body.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...commons.types.map_value import MapValue +from ...core.serialization import FieldMetadata +from .ingestion_usage import IngestionUsage +from .update_span_body import UpdateSpanBody +from .usage_details import UsageDetails + + +class UpdateGenerationBody(UpdateSpanBody): + completion_start_time: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="completionStartTime") + ] = None + model: typing.Optional[str] = None + model_parameters: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, MapValue]], + FieldMetadata(alias="modelParameters"), + ] = None + usage: typing.Optional[IngestionUsage] = None + prompt_name: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="promptName") + ] = None + usage_details: typing_extensions.Annotated[ + typing.Optional[UsageDetails], FieldMetadata(alias="usageDetails") + ] = None + cost_details: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, float]], FieldMetadata(alias="costDetails") + ] = None + prompt_version: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="promptVersion") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/update_generation_event.py b/langfuse/api/ingestion/types/update_generation_event.py new file mode 100644 index 000000000..e2c7fe284 --- /dev/null +++ b/langfuse/api/ingestion/types/update_generation_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .update_generation_body import UpdateGenerationBody + + +class UpdateGenerationEvent(BaseEvent): + body: UpdateGenerationBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/update_observation_event.py b/langfuse/api/ingestion/types/update_observation_event.py new file mode 100644 index 000000000..5c33e7591 --- /dev/null +++ b/langfuse/api/ingestion/types/update_observation_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .observation_body import ObservationBody + + +class UpdateObservationEvent(BaseEvent): + body: ObservationBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/update_span_body.py b/langfuse/api/ingestion/types/update_span_body.py new file mode 100644 index 000000000..f094b7cdb --- /dev/null +++ b/langfuse/api/ingestion/types/update_span_body.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.serialization import FieldMetadata +from .update_event_body import UpdateEventBody + + +class UpdateSpanBody(UpdateEventBody): + end_time: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="endTime") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/update_span_event.py b/langfuse/api/ingestion/types/update_span_event.py new file mode 100644 index 000000000..20214ac9d --- /dev/null +++ b/langfuse/api/ingestion/types/update_span_event.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_event import BaseEvent +from .update_span_body import UpdateSpanBody + + +class UpdateSpanEvent(BaseEvent): + body: UpdateSpanBody + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/resources/ingestion/types/usage_details.py b/langfuse/api/ingestion/types/usage_details.py similarity index 100% rename from langfuse/api/resources/ingestion/types/usage_details.py rename to langfuse/api/ingestion/types/usage_details.py diff --git a/langfuse/api/legacy/__init__.py b/langfuse/api/legacy/__init__.py new file mode 100644 index 000000000..0a67d1c0c --- /dev/null +++ b/langfuse/api/legacy/__init__.py @@ -0,0 +1,63 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from . import metrics_v1, observations_v1, score_v1 + from .metrics_v1 import MetricsResponse + from .observations_v1 import Observations, ObservationsViews + from .score_v1 import CreateScoreRequest, CreateScoreResponse, CreateScoreSource +_dynamic_imports: typing.Dict[str, str] = { + "CreateScoreRequest": ".score_v1", + "CreateScoreResponse": ".score_v1", + "CreateScoreSource": ".score_v1", + "MetricsResponse": ".metrics_v1", + "Observations": ".observations_v1", + "ObservationsViews": ".observations_v1", + "metrics_v1": ".metrics_v1", + "observations_v1": ".observations_v1", + "score_v1": ".score_v1", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CreateScoreRequest", + "CreateScoreResponse", + "CreateScoreSource", + "MetricsResponse", + "Observations", + "ObservationsViews", + "metrics_v1", + "observations_v1", + "score_v1", +] diff --git a/langfuse/api/legacy/client.py b/langfuse/api/legacy/client.py new file mode 100644 index 000000000..3886b9eac --- /dev/null +++ b/langfuse/api/legacy/client.py @@ -0,0 +1,105 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .raw_client import AsyncRawLegacyClient, RawLegacyClient + +if typing.TYPE_CHECKING: + from .metrics_v1.client import AsyncMetricsV1Client, MetricsV1Client + from .observations_v1.client import AsyncObservationsV1Client, ObservationsV1Client + from .score_v1.client import AsyncScoreV1Client, ScoreV1Client + + +class LegacyClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawLegacyClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._metrics_v1: typing.Optional[MetricsV1Client] = None + self._observations_v1: typing.Optional[ObservationsV1Client] = None + self._score_v1: typing.Optional[ScoreV1Client] = None + + @property + def with_raw_response(self) -> RawLegacyClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawLegacyClient + """ + return self._raw_client + + @property + def metrics_v1(self): + if self._metrics_v1 is None: + from .metrics_v1.client import MetricsV1Client # noqa: E402 + + self._metrics_v1 = MetricsV1Client(client_wrapper=self._client_wrapper) + return self._metrics_v1 + + @property + def observations_v1(self): + if self._observations_v1 is None: + from .observations_v1.client import ObservationsV1Client # noqa: E402 + + self._observations_v1 = ObservationsV1Client( + client_wrapper=self._client_wrapper + ) + return self._observations_v1 + + @property + def score_v1(self): + if self._score_v1 is None: + from .score_v1.client import ScoreV1Client # noqa: E402 + + self._score_v1 = ScoreV1Client(client_wrapper=self._client_wrapper) + return self._score_v1 + + +class AsyncLegacyClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawLegacyClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._metrics_v1: typing.Optional[AsyncMetricsV1Client] = None + self._observations_v1: typing.Optional[AsyncObservationsV1Client] = None + self._score_v1: typing.Optional[AsyncScoreV1Client] = None + + @property + def with_raw_response(self) -> AsyncRawLegacyClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawLegacyClient + """ + return self._raw_client + + @property + def metrics_v1(self): + if self._metrics_v1 is None: + from .metrics_v1.client import AsyncMetricsV1Client # noqa: E402 + + self._metrics_v1 = AsyncMetricsV1Client(client_wrapper=self._client_wrapper) + return self._metrics_v1 + + @property + def observations_v1(self): + if self._observations_v1 is None: + from .observations_v1.client import AsyncObservationsV1Client # noqa: E402 + + self._observations_v1 = AsyncObservationsV1Client( + client_wrapper=self._client_wrapper + ) + return self._observations_v1 + + @property + def score_v1(self): + if self._score_v1 is None: + from .score_v1.client import AsyncScoreV1Client # noqa: E402 + + self._score_v1 = AsyncScoreV1Client(client_wrapper=self._client_wrapper) + return self._score_v1 diff --git a/langfuse/api/legacy/metrics_v1/__init__.py b/langfuse/api/legacy/metrics_v1/__init__.py new file mode 100644 index 000000000..fb47bc976 --- /dev/null +++ b/langfuse/api/legacy/metrics_v1/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import MetricsResponse +_dynamic_imports: typing.Dict[str, str] = {"MetricsResponse": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["MetricsResponse"] diff --git a/langfuse/api/resources/metrics/client.py b/langfuse/api/legacy/metrics_v1/client.py similarity index 63% rename from langfuse/api/resources/metrics/client.py rename to langfuse/api/legacy/metrics_v1/client.py index f46dc75f2..bece982b3 100644 --- a/langfuse/api/resources/metrics/client.py +++ b/langfuse/api/legacy/metrics_v1/client.py @@ -1,29 +1,37 @@ # This file was auto-generated by Fern from our API Definition. import typing -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.pydantic_utilities import pydantic_v1 from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError +from .raw_client import AsyncRawMetricsV1Client, RawMetricsV1Client from .types.metrics_response import MetricsResponse -class MetricsClient: +class MetricsV1Client: def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper + self._raw_client = RawMetricsV1Client(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawMetricsV1Client: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawMetricsV1Client + """ + return self._raw_client def metrics( self, *, query: str, request_options: typing.Optional[RequestOptions] = None ) -> MetricsResponse: """ - Get metrics from the Langfuse project using a query object + Get metrics from the Langfuse project using a query object. + + Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance. + + For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). Parameters ---------- @@ -79,9 +87,9 @@ def metrics( Examples -------- - from langfuse.client import FernLangfuse + from langfuse import LangfuseAPI - client = FernLangfuse( + client = LangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -89,52 +97,40 @@ def metrics( password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", ) - client.metrics.metrics( + client.legacy.metrics_v1.metrics( query="query", ) """ - _response = self._client_wrapper.httpx_client.request( - "api/public/metrics", - method="GET", - params={"query": query}, - request_options=request_options, + _response = self._raw_client.metrics( + query=query, request_options=request_options ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MetricsResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncMetricsClient: + return _response.data + + +class AsyncMetricsV1Client: def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper + self._raw_client = AsyncRawMetricsV1Client(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawMetricsV1Client: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawMetricsV1Client + """ + return self._raw_client async def metrics( self, *, query: str, request_options: typing.Optional[RequestOptions] = None ) -> MetricsResponse: """ - Get metrics from the Langfuse project using a query object + Get metrics from the Langfuse project using a query object. + + Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance. + + For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). Parameters ---------- @@ -192,9 +188,9 @@ async def metrics( -------- import asyncio - from langfuse.client import AsyncFernLangfuse + from langfuse import AsyncLangfuseAPI - client = AsyncFernLangfuse( + client = AsyncLangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -205,41 +201,14 @@ async def metrics( async def main() -> None: - await client.metrics.metrics( + await client.legacy.metrics_v1.metrics( query="query", ) asyncio.run(main()) """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/metrics", - method="GET", - params={"query": query}, - request_options=request_options, + _response = await self._raw_client.metrics( + query=query, request_options=request_options ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MetricsResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) + return _response.data diff --git a/langfuse/api/legacy/metrics_v1/raw_client.py b/langfuse/api/legacy/metrics_v1/raw_client.py new file mode 100644 index 000000000..61f03e541 --- /dev/null +++ b/langfuse/api/legacy/metrics_v1/raw_client.py @@ -0,0 +1,322 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...commons.errors.access_denied_error import AccessDeniedError +from ...commons.errors.error import Error +from ...commons.errors.method_not_allowed_error import MethodNotAllowedError +from ...commons.errors.not_found_error import NotFoundError +from ...commons.errors.unauthorized_error import UnauthorizedError +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +from .types.metrics_response import MetricsResponse + + +class RawMetricsV1Client: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def metrics( + self, *, query: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[MetricsResponse]: + """ + Get metrics from the Langfuse project using a query object. + + Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance. + + For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). + + Parameters + ---------- + query : str + JSON string containing the query parameters with the following structure: + ```json + { + "view": string, // Required. One of "traces", "observations", "scores-numeric", "scores-categorical" + "dimensions": [ // Optional. Default: [] + { + "field": string // Field to group by, e.g. "name", "userId", "sessionId" + } + ], + "metrics": [ // Required. At least one metric must be provided + { + "measure": string, // What to measure, e.g. "count", "latency", "value" + "aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram" + } + ], + "filters": [ // Optional. Default: [] + { + "column": string, // Column to filter on + "operator": string, // Operator, e.g. "=", ">", "<", "contains" + "value": any, // Value to compare against + "type": string, // Data type, e.g. "string", "number", "stringObject" + "key": string // Required only when filtering on metadata + } + ], + "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time + "granularity": string // One of "minute", "hour", "day", "week", "month", "auto" + }, + "fromTimestamp": string, // Required. ISO datetime string for start of time range + "toTimestamp": string, // Required. ISO datetime string for end of time range + "orderBy": [ // Optional. Default: null + { + "field": string, // Field to order by + "direction": string // "asc" or "desc" + } + ], + "config": { // Optional. Query-specific configuration + "bins": number, // Optional. Number of bins for histogram (1-100), default: 10 + "row_limit": number // Optional. Row limit for results (1-1000) + } + } + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MetricsResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/metrics", + method="GET", + params={ + "query": query, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MetricsResponse, + parse_obj_as( + type_=MetricsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawMetricsV1Client: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def metrics( + self, *, query: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[MetricsResponse]: + """ + Get metrics from the Langfuse project using a query object. + + Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance. + + For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). + + Parameters + ---------- + query : str + JSON string containing the query parameters with the following structure: + ```json + { + "view": string, // Required. One of "traces", "observations", "scores-numeric", "scores-categorical" + "dimensions": [ // Optional. Default: [] + { + "field": string // Field to group by, e.g. "name", "userId", "sessionId" + } + ], + "metrics": [ // Required. At least one metric must be provided + { + "measure": string, // What to measure, e.g. "count", "latency", "value" + "aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram" + } + ], + "filters": [ // Optional. Default: [] + { + "column": string, // Column to filter on + "operator": string, // Operator, e.g. "=", ">", "<", "contains" + "value": any, // Value to compare against + "type": string, // Data type, e.g. "string", "number", "stringObject" + "key": string // Required only when filtering on metadata + } + ], + "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time + "granularity": string // One of "minute", "hour", "day", "week", "month", "auto" + }, + "fromTimestamp": string, // Required. ISO datetime string for start of time range + "toTimestamp": string, // Required. ISO datetime string for end of time range + "orderBy": [ // Optional. Default: null + { + "field": string, // Field to order by + "direction": string // "asc" or "desc" + } + ], + "config": { // Optional. Query-specific configuration + "bins": number, // Optional. Number of bins for histogram (1-100), default: 10 + "row_limit": number // Optional. Row limit for results (1-1000) + } + } + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MetricsResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/metrics", + method="GET", + params={ + "query": query, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MetricsResponse, + parse_obj_as( + type_=MetricsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/legacy/metrics_v1/types/__init__.py b/langfuse/api/legacy/metrics_v1/types/__init__.py new file mode 100644 index 000000000..308847504 --- /dev/null +++ b/langfuse/api/legacy/metrics_v1/types/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .metrics_response import MetricsResponse +_dynamic_imports: typing.Dict[str, str] = {"MetricsResponse": ".metrics_response"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["MetricsResponse"] diff --git a/langfuse/api/legacy/metrics_v1/types/metrics_response.py b/langfuse/api/legacy/metrics_v1/types/metrics_response.py new file mode 100644 index 000000000..734c47ffd --- /dev/null +++ b/langfuse/api/legacy/metrics_v1/types/metrics_response.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel + + +class MetricsResponse(UniversalBaseModel): + data: typing.List[typing.Dict[str, typing.Any]] = pydantic.Field() + """ + The metrics data. Each item in the list contains the metric values and dimensions requested in the query. + Format varies based on the query parameters. + Histograms will return an array with [lower, upper, height] tuples. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/legacy/observations_v1/__init__.py b/langfuse/api/legacy/observations_v1/__init__.py new file mode 100644 index 000000000..22b445984 --- /dev/null +++ b/langfuse/api/legacy/observations_v1/__init__.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import Observations, ObservationsViews +_dynamic_imports: typing.Dict[str, str] = { + "Observations": ".types", + "ObservationsViews": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["Observations", "ObservationsViews"] diff --git a/langfuse/api/legacy/observations_v1/client.py b/langfuse/api/legacy/observations_v1/client.py new file mode 100644 index 000000000..40d5df6b8 --- /dev/null +++ b/langfuse/api/legacy/observations_v1/client.py @@ -0,0 +1,523 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ...commons.types.observation_level import ObservationLevel +from ...commons.types.observations_view import ObservationsView +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from .raw_client import AsyncRawObservationsV1Client, RawObservationsV1Client +from .types.observations_views import ObservationsViews + + +class ObservationsV1Client: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawObservationsV1Client(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawObservationsV1Client: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawObservationsV1Client + """ + return self._raw_client + + def get( + self, + observation_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> ObservationsView: + """ + Get a observation + + Parameters + ---------- + observation_id : str + The unique langfuse identifier of an observation, can be an event, span or generation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ObservationsView + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.legacy.observations_v1.get( + observation_id="observationId", + ) + """ + _response = self._raw_client.get( + observation_id, request_options=request_options + ) + return _response.data + + def get_many( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + name: typing.Optional[str] = None, + user_id: typing.Optional[str] = None, + type: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + level: typing.Optional[ObservationLevel] = None, + parent_observation_id: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + from_start_time: typing.Optional[dt.datetime] = None, + to_start_time: typing.Optional[dt.datetime] = None, + version: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ObservationsViews: + """ + Get a list of observations. + + Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection. + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + name : typing.Optional[str] + + user_id : typing.Optional[str] + + type : typing.Optional[str] + + trace_id : typing.Optional[str] + + level : typing.Optional[ObservationLevel] + Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). + + parent_observation_id : typing.Optional[str] + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for observations where the environment is one of the provided values. + + from_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time on or after this datetime (ISO 8601). + + to_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time before this datetime (ISO 8601). + + version : typing.Optional[str] + Optional filter to only include observations with a certain version. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Observation Fields + - `id` (string) - Observation ID + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + - `name` (string) - Observation name + - `traceId` (string) - Associated trace ID + - `startTime` (datetime) - Observation start time + - `endTime` (datetime) - Observation end time + - `environment` (string) - Environment tag + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + - `statusMessage` (string) - Status message + - `version` (string) - Version tag + + ### Performance Metrics + - `latency` (number) - Latency in seconds (calculated: end_time - start_time) + - `timeToFirstToken` (number) - Time to first token in seconds + - `tokensPerSecond` (number) - Output tokens per second + + ### Token Usage + - `inputTokens` (number) - Number of input tokens + - `outputTokens` (number) - Number of output tokens + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + ### Cost Metrics + - `inputCost` (number) - Input cost in USD + - `outputCost` (number) - Output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Model Information + - `model` (string) - Provided model name + - `promptName` (string) - Associated prompt name + - `promptVersion` (number) - Associated prompt version + + ### Structured Data + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + ### Associated Trace Fields (requires join with traces table) + - `userId` (string) - User ID from associated trace + - `traceName` (string) - Name from associated trace + - `traceEnvironment` (string) - Environment from associated trace + - `traceTags` (arrayOptions) - Tags from associated trace + + ## Filter Examples + ```json + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + } + ] + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ObservationsViews + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.legacy.observations_v1.get_many() + """ + _response = self._raw_client.get_many( + page=page, + limit=limit, + name=name, + user_id=user_id, + type=type, + trace_id=trace_id, + level=level, + parent_observation_id=parent_observation_id, + environment=environment, + from_start_time=from_start_time, + to_start_time=to_start_time, + version=version, + filter=filter, + request_options=request_options, + ) + return _response.data + + +class AsyncObservationsV1Client: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawObservationsV1Client(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawObservationsV1Client: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawObservationsV1Client + """ + return self._raw_client + + async def get( + self, + observation_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> ObservationsView: + """ + Get a observation + + Parameters + ---------- + observation_id : str + The unique langfuse identifier of an observation, can be an event, span or generation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ObservationsView + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.legacy.observations_v1.get( + observation_id="observationId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + observation_id, request_options=request_options + ) + return _response.data + + async def get_many( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + name: typing.Optional[str] = None, + user_id: typing.Optional[str] = None, + type: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + level: typing.Optional[ObservationLevel] = None, + parent_observation_id: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + from_start_time: typing.Optional[dt.datetime] = None, + to_start_time: typing.Optional[dt.datetime] = None, + version: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ObservationsViews: + """ + Get a list of observations. + + Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection. + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + name : typing.Optional[str] + + user_id : typing.Optional[str] + + type : typing.Optional[str] + + trace_id : typing.Optional[str] + + level : typing.Optional[ObservationLevel] + Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). + + parent_observation_id : typing.Optional[str] + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for observations where the environment is one of the provided values. + + from_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time on or after this datetime (ISO 8601). + + to_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time before this datetime (ISO 8601). + + version : typing.Optional[str] + Optional filter to only include observations with a certain version. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Observation Fields + - `id` (string) - Observation ID + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + - `name` (string) - Observation name + - `traceId` (string) - Associated trace ID + - `startTime` (datetime) - Observation start time + - `endTime` (datetime) - Observation end time + - `environment` (string) - Environment tag + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + - `statusMessage` (string) - Status message + - `version` (string) - Version tag + + ### Performance Metrics + - `latency` (number) - Latency in seconds (calculated: end_time - start_time) + - `timeToFirstToken` (number) - Time to first token in seconds + - `tokensPerSecond` (number) - Output tokens per second + + ### Token Usage + - `inputTokens` (number) - Number of input tokens + - `outputTokens` (number) - Number of output tokens + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + ### Cost Metrics + - `inputCost` (number) - Input cost in USD + - `outputCost` (number) - Output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Model Information + - `model` (string) - Provided model name + - `promptName` (string) - Associated prompt name + - `promptVersion` (number) - Associated prompt version + + ### Structured Data + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + ### Associated Trace Fields (requires join with traces table) + - `userId` (string) - User ID from associated trace + - `traceName` (string) - Name from associated trace + - `traceEnvironment` (string) - Environment from associated trace + - `traceTags` (arrayOptions) - Tags from associated trace + + ## Filter Examples + ```json + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + } + ] + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ObservationsViews + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.legacy.observations_v1.get_many() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_many( + page=page, + limit=limit, + name=name, + user_id=user_id, + type=type, + trace_id=trace_id, + level=level, + parent_observation_id=parent_observation_id, + environment=environment, + from_start_time=from_start_time, + to_start_time=to_start_time, + version=version, + filter=filter, + request_options=request_options, + ) + return _response.data diff --git a/langfuse/api/legacy/observations_v1/raw_client.py b/langfuse/api/legacy/observations_v1/raw_client.py new file mode 100644 index 000000000..61ecf409d --- /dev/null +++ b/langfuse/api/legacy/observations_v1/raw_client.py @@ -0,0 +1,759 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ...commons.errors.access_denied_error import AccessDeniedError +from ...commons.errors.error import Error +from ...commons.errors.method_not_allowed_error import MethodNotAllowedError +from ...commons.errors.not_found_error import NotFoundError +from ...commons.errors.unauthorized_error import UnauthorizedError +from ...commons.types.observation_level import ObservationLevel +from ...commons.types.observations_view import ObservationsView +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.datetime_utils import serialize_datetime +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import jsonable_encoder +from ...core.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +from .types.observations_views import ObservationsViews + + +class RawObservationsV1Client: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get( + self, + observation_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ObservationsView]: + """ + Get a observation + + Parameters + ---------- + observation_id : str + The unique langfuse identifier of an observation, can be an event, span or generation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ObservationsView] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/observations/{jsonable_encoder(observation_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ObservationsView, + parse_obj_as( + type_=ObservationsView, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_many( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + name: typing.Optional[str] = None, + user_id: typing.Optional[str] = None, + type: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + level: typing.Optional[ObservationLevel] = None, + parent_observation_id: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + from_start_time: typing.Optional[dt.datetime] = None, + to_start_time: typing.Optional[dt.datetime] = None, + version: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ObservationsViews]: + """ + Get a list of observations. + + Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection. + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + name : typing.Optional[str] + + user_id : typing.Optional[str] + + type : typing.Optional[str] + + trace_id : typing.Optional[str] + + level : typing.Optional[ObservationLevel] + Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). + + parent_observation_id : typing.Optional[str] + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for observations where the environment is one of the provided values. + + from_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time on or after this datetime (ISO 8601). + + to_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time before this datetime (ISO 8601). + + version : typing.Optional[str] + Optional filter to only include observations with a certain version. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Observation Fields + - `id` (string) - Observation ID + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + - `name` (string) - Observation name + - `traceId` (string) - Associated trace ID + - `startTime` (datetime) - Observation start time + - `endTime` (datetime) - Observation end time + - `environment` (string) - Environment tag + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + - `statusMessage` (string) - Status message + - `version` (string) - Version tag + + ### Performance Metrics + - `latency` (number) - Latency in seconds (calculated: end_time - start_time) + - `timeToFirstToken` (number) - Time to first token in seconds + - `tokensPerSecond` (number) - Output tokens per second + + ### Token Usage + - `inputTokens` (number) - Number of input tokens + - `outputTokens` (number) - Number of output tokens + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + ### Cost Metrics + - `inputCost` (number) - Input cost in USD + - `outputCost` (number) - Output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Model Information + - `model` (string) - Provided model name + - `promptName` (string) - Associated prompt name + - `promptVersion` (number) - Associated prompt version + + ### Structured Data + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + ### Associated Trace Fields (requires join with traces table) + - `userId` (string) - User ID from associated trace + - `traceName` (string) - Name from associated trace + - `traceEnvironment` (string) - Environment from associated trace + - `traceTags` (arrayOptions) - Tags from associated trace + + ## Filter Examples + ```json + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + } + ] + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ObservationsViews] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/observations", + method="GET", + params={ + "page": page, + "limit": limit, + "name": name, + "userId": user_id, + "type": type, + "traceId": trace_id, + "level": level, + "parentObservationId": parent_observation_id, + "environment": environment, + "fromStartTime": serialize_datetime(from_start_time) + if from_start_time is not None + else None, + "toStartTime": serialize_datetime(to_start_time) + if to_start_time is not None + else None, + "version": version, + "filter": filter, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ObservationsViews, + parse_obj_as( + type_=ObservationsViews, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawObservationsV1Client: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get( + self, + observation_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ObservationsView]: + """ + Get a observation + + Parameters + ---------- + observation_id : str + The unique langfuse identifier of an observation, can be an event, span or generation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ObservationsView] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/observations/{jsonable_encoder(observation_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ObservationsView, + parse_obj_as( + type_=ObservationsView, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_many( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + name: typing.Optional[str] = None, + user_id: typing.Optional[str] = None, + type: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + level: typing.Optional[ObservationLevel] = None, + parent_observation_id: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + from_start_time: typing.Optional[dt.datetime] = None, + to_start_time: typing.Optional[dt.datetime] = None, + version: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ObservationsViews]: + """ + Get a list of observations. + + Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection. + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + name : typing.Optional[str] + + user_id : typing.Optional[str] + + type : typing.Optional[str] + + trace_id : typing.Optional[str] + + level : typing.Optional[ObservationLevel] + Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). + + parent_observation_id : typing.Optional[str] + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for observations where the environment is one of the provided values. + + from_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time on or after this datetime (ISO 8601). + + to_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time before this datetime (ISO 8601). + + version : typing.Optional[str] + Optional filter to only include observations with a certain version. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Observation Fields + - `id` (string) - Observation ID + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + - `name` (string) - Observation name + - `traceId` (string) - Associated trace ID + - `startTime` (datetime) - Observation start time + - `endTime` (datetime) - Observation end time + - `environment` (string) - Environment tag + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + - `statusMessage` (string) - Status message + - `version` (string) - Version tag + + ### Performance Metrics + - `latency` (number) - Latency in seconds (calculated: end_time - start_time) + - `timeToFirstToken` (number) - Time to first token in seconds + - `tokensPerSecond` (number) - Output tokens per second + + ### Token Usage + - `inputTokens` (number) - Number of input tokens + - `outputTokens` (number) - Number of output tokens + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + ### Cost Metrics + - `inputCost` (number) - Input cost in USD + - `outputCost` (number) - Output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Model Information + - `model` (string) - Provided model name + - `promptName` (string) - Associated prompt name + - `promptVersion` (number) - Associated prompt version + + ### Structured Data + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + ### Associated Trace Fields (requires join with traces table) + - `userId` (string) - User ID from associated trace + - `traceName` (string) - Name from associated trace + - `traceEnvironment` (string) - Environment from associated trace + - `traceTags` (arrayOptions) - Tags from associated trace + + ## Filter Examples + ```json + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + } + ] + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ObservationsViews] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/observations", + method="GET", + params={ + "page": page, + "limit": limit, + "name": name, + "userId": user_id, + "type": type, + "traceId": trace_id, + "level": level, + "parentObservationId": parent_observation_id, + "environment": environment, + "fromStartTime": serialize_datetime(from_start_time) + if from_start_time is not None + else None, + "toStartTime": serialize_datetime(to_start_time) + if to_start_time is not None + else None, + "version": version, + "filter": filter, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ObservationsViews, + parse_obj_as( + type_=ObservationsViews, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/legacy/observations_v1/types/__init__.py b/langfuse/api/legacy/observations_v1/types/__init__.py new file mode 100644 index 000000000..247b674a1 --- /dev/null +++ b/langfuse/api/legacy/observations_v1/types/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .observations import Observations + from .observations_views import ObservationsViews +_dynamic_imports: typing.Dict[str, str] = { + "Observations": ".observations", + "ObservationsViews": ".observations_views", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["Observations", "ObservationsViews"] diff --git a/langfuse/api/legacy/observations_v1/types/observations.py b/langfuse/api/legacy/observations_v1/types/observations.py new file mode 100644 index 000000000..860047ef2 --- /dev/null +++ b/langfuse/api/legacy/observations_v1/types/observations.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....commons.types.observation import Observation +from ....core.pydantic_utilities import UniversalBaseModel +from ....utils.pagination.types.meta_response import MetaResponse + + +class Observations(UniversalBaseModel): + data: typing.List[Observation] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/legacy/observations_v1/types/observations_views.py b/langfuse/api/legacy/observations_v1/types/observations_views.py new file mode 100644 index 000000000..b6d294c7d --- /dev/null +++ b/langfuse/api/legacy/observations_v1/types/observations_views.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....commons.types.observations_view import ObservationsView +from ....core.pydantic_utilities import UniversalBaseModel +from ....utils.pagination.types.meta_response import MetaResponse + + +class ObservationsViews(UniversalBaseModel): + data: typing.List[ObservationsView] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/legacy/raw_client.py b/langfuse/api/legacy/raw_client.py new file mode 100644 index 000000000..0672ed01d --- /dev/null +++ b/langfuse/api/legacy/raw_client.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper + + +class RawLegacyClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + +class AsyncRawLegacyClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper diff --git a/langfuse/api/legacy/score_v1/__init__.py b/langfuse/api/legacy/score_v1/__init__.py new file mode 100644 index 000000000..4841a9656 --- /dev/null +++ b/langfuse/api/legacy/score_v1/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import CreateScoreRequest, CreateScoreResponse, CreateScoreSource +_dynamic_imports: typing.Dict[str, str] = { + "CreateScoreRequest": ".types", + "CreateScoreResponse": ".types", + "CreateScoreSource": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateScoreRequest", "CreateScoreResponse", "CreateScoreSource"] diff --git a/langfuse/api/legacy/score_v1/client.py b/langfuse/api/legacy/score_v1/client.py new file mode 100644 index 000000000..60f118747 --- /dev/null +++ b/langfuse/api/legacy/score_v1/client.py @@ -0,0 +1,340 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...commons.types.create_score_value import CreateScoreValue +from ...commons.types.score_data_type import ScoreDataType +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from .raw_client import AsyncRawScoreV1Client, RawScoreV1Client +from .types.create_score_response import CreateScoreResponse +from .types.create_score_source import CreateScoreSource + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ScoreV1Client: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawScoreV1Client(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawScoreV1Client: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawScoreV1Client + """ + return self._raw_client + + def create( + self, + *, + name: str, + value: CreateScoreValue, + id: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + observation_id: typing.Optional[str] = OMIT, + dataset_run_id: typing.Optional[str] = OMIT, + comment: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + environment: typing.Optional[str] = OMIT, + queue_id: typing.Optional[str] = OMIT, + data_type: typing.Optional[ScoreDataType] = OMIT, + config_id: typing.Optional[str] = OMIT, + source: typing.Optional[CreateScoreSource] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateScoreResponse: + """ + Create a score (supports both trace and session scores) + + Parameters + ---------- + name : str + + value : CreateScoreValue + The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. + + id : typing.Optional[str] + + trace_id : typing.Optional[str] + + session_id : typing.Optional[str] + + observation_id : typing.Optional[str] + + dataset_run_id : typing.Optional[str] + + comment : typing.Optional[str] + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + + environment : typing.Optional[str] + The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + + queue_id : typing.Optional[str] + The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + + data_type : typing.Optional[ScoreDataType] + The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. + + config_id : typing.Optional[str] + Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. + + source : typing.Optional[CreateScoreSource] + The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateScoreResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.legacy.score_v1.create( + name="name", + value=1.1, + ) + """ + _response = self._raw_client.create( + name=name, + value=value, + id=id, + trace_id=trace_id, + session_id=session_id, + observation_id=observation_id, + dataset_run_id=dataset_run_id, + comment=comment, + metadata=metadata, + environment=environment, + queue_id=queue_id, + data_type=data_type, + config_id=config_id, + source=source, + request_options=request_options, + ) + return _response.data + + def delete( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Delete a score (supports both trace and session scores) + + Parameters + ---------- + score_id : str + The unique langfuse identifier of a score + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.legacy.score_v1.delete( + score_id="scoreId", + ) + """ + _response = self._raw_client.delete(score_id, request_options=request_options) + return _response.data + + +class AsyncScoreV1Client: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawScoreV1Client(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawScoreV1Client: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawScoreV1Client + """ + return self._raw_client + + async def create( + self, + *, + name: str, + value: CreateScoreValue, + id: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + observation_id: typing.Optional[str] = OMIT, + dataset_run_id: typing.Optional[str] = OMIT, + comment: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + environment: typing.Optional[str] = OMIT, + queue_id: typing.Optional[str] = OMIT, + data_type: typing.Optional[ScoreDataType] = OMIT, + config_id: typing.Optional[str] = OMIT, + source: typing.Optional[CreateScoreSource] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateScoreResponse: + """ + Create a score (supports both trace and session scores) + + Parameters + ---------- + name : str + + value : CreateScoreValue + The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. + + id : typing.Optional[str] + + trace_id : typing.Optional[str] + + session_id : typing.Optional[str] + + observation_id : typing.Optional[str] + + dataset_run_id : typing.Optional[str] + + comment : typing.Optional[str] + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + + environment : typing.Optional[str] + The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + + queue_id : typing.Optional[str] + The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + + data_type : typing.Optional[ScoreDataType] + The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. + + config_id : typing.Optional[str] + Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. + + source : typing.Optional[CreateScoreSource] + The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateScoreResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.legacy.score_v1.create( + name="name", + value=1.1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + name=name, + value=value, + id=id, + trace_id=trace_id, + session_id=session_id, + observation_id=observation_id, + dataset_run_id=dataset_run_id, + comment=comment, + metadata=metadata, + environment=environment, + queue_id=queue_id, + data_type=data_type, + config_id=config_id, + source=source, + request_options=request_options, + ) + return _response.data + + async def delete( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Delete a score (supports both trace and session scores) + + Parameters + ---------- + score_id : str + The unique langfuse identifier of a score + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.legacy.score_v1.delete( + score_id="scoreId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete( + score_id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/legacy/score_v1/raw_client.py b/langfuse/api/legacy/score_v1/raw_client.py new file mode 100644 index 000000000..3dc0164e0 --- /dev/null +++ b/langfuse/api/legacy/score_v1/raw_client.py @@ -0,0 +1,556 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...commons.errors.access_denied_error import AccessDeniedError +from ...commons.errors.error import Error +from ...commons.errors.method_not_allowed_error import MethodNotAllowedError +from ...commons.errors.not_found_error import NotFoundError +from ...commons.errors.unauthorized_error import UnauthorizedError +from ...commons.types.create_score_value import CreateScoreValue +from ...commons.types.score_data_type import ScoreDataType +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import jsonable_encoder +from ...core.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from .types.create_score_response import CreateScoreResponse +from .types.create_score_source import CreateScoreSource + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawScoreV1Client: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + name: str, + value: CreateScoreValue, + id: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + observation_id: typing.Optional[str] = OMIT, + dataset_run_id: typing.Optional[str] = OMIT, + comment: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + environment: typing.Optional[str] = OMIT, + queue_id: typing.Optional[str] = OMIT, + data_type: typing.Optional[ScoreDataType] = OMIT, + config_id: typing.Optional[str] = OMIT, + source: typing.Optional[CreateScoreSource] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[CreateScoreResponse]: + """ + Create a score (supports both trace and session scores) + + Parameters + ---------- + name : str + + value : CreateScoreValue + The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. + + id : typing.Optional[str] + + trace_id : typing.Optional[str] + + session_id : typing.Optional[str] + + observation_id : typing.Optional[str] + + dataset_run_id : typing.Optional[str] + + comment : typing.Optional[str] + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + + environment : typing.Optional[str] + The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + + queue_id : typing.Optional[str] + The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + + data_type : typing.Optional[ScoreDataType] + The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. + + config_id : typing.Optional[str] + Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. + + source : typing.Optional[CreateScoreSource] + The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[CreateScoreResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/scores", + method="POST", + json={ + "id": id, + "traceId": trace_id, + "sessionId": session_id, + "observationId": observation_id, + "datasetRunId": dataset_run_id, + "name": name, + "value": convert_and_respect_annotation_metadata( + object_=value, annotation=CreateScoreValue, direction="write" + ), + "comment": comment, + "metadata": metadata, + "environment": environment, + "queueId": queue_id, + "dataType": data_type, + "configId": config_id, + "source": source, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateScoreResponse, + parse_obj_as( + type_=CreateScoreResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Delete a score (supports both trace and session scores) + + Parameters + ---------- + score_id : str + The unique langfuse identifier of a score + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/scores/{jsonable_encoder(score_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawScoreV1Client: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + name: str, + value: CreateScoreValue, + id: typing.Optional[str] = OMIT, + trace_id: typing.Optional[str] = OMIT, + session_id: typing.Optional[str] = OMIT, + observation_id: typing.Optional[str] = OMIT, + dataset_run_id: typing.Optional[str] = OMIT, + comment: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + environment: typing.Optional[str] = OMIT, + queue_id: typing.Optional[str] = OMIT, + data_type: typing.Optional[ScoreDataType] = OMIT, + config_id: typing.Optional[str] = OMIT, + source: typing.Optional[CreateScoreSource] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[CreateScoreResponse]: + """ + Create a score (supports both trace and session scores) + + Parameters + ---------- + name : str + + value : CreateScoreValue + The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. + + id : typing.Optional[str] + + trace_id : typing.Optional[str] + + session_id : typing.Optional[str] + + observation_id : typing.Optional[str] + + dataset_run_id : typing.Optional[str] + + comment : typing.Optional[str] + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + + environment : typing.Optional[str] + The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + + queue_id : typing.Optional[str] + The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + + data_type : typing.Optional[ScoreDataType] + The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. + + config_id : typing.Optional[str] + Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. + + source : typing.Optional[CreateScoreSource] + The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[CreateScoreResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/scores", + method="POST", + json={ + "id": id, + "traceId": trace_id, + "sessionId": session_id, + "observationId": observation_id, + "datasetRunId": dataset_run_id, + "name": name, + "value": convert_and_respect_annotation_metadata( + object_=value, annotation=CreateScoreValue, direction="write" + ), + "comment": comment, + "metadata": metadata, + "environment": environment, + "queueId": queue_id, + "dataType": data_type, + "configId": config_id, + "source": source, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateScoreResponse, + parse_obj_as( + type_=CreateScoreResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Delete a score (supports both trace and session scores) + + Parameters + ---------- + score_id : str + The unique langfuse identifier of a score + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/scores/{jsonable_encoder(score_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/legacy/score_v1/types/__init__.py b/langfuse/api/legacy/score_v1/types/__init__.py new file mode 100644 index 000000000..dde25cbb4 --- /dev/null +++ b/langfuse/api/legacy/score_v1/types/__init__.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_score_request import CreateScoreRequest + from .create_score_response import CreateScoreResponse + from .create_score_source import CreateScoreSource +_dynamic_imports: typing.Dict[str, str] = { + "CreateScoreRequest": ".create_score_request", + "CreateScoreResponse": ".create_score_response", + "CreateScoreSource": ".create_score_source", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateScoreRequest", "CreateScoreResponse", "CreateScoreSource"] diff --git a/langfuse/api/legacy/score_v1/types/create_score_request.py b/langfuse/api/legacy/score_v1/types/create_score_request.py new file mode 100644 index 000000000..ef498fe6c --- /dev/null +++ b/langfuse/api/legacy/score_v1/types/create_score_request.py @@ -0,0 +1,81 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....commons.types.create_score_value import CreateScoreValue +from ....commons.types.score_data_type import ScoreDataType +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .create_score_source import CreateScoreSource + + +class CreateScoreRequest(UniversalBaseModel): + """ + Examples + -------- + from langfuse.legacy.score_v1 import CreateScoreRequest + + CreateScoreRequest( + name="novelty", + value=0.9, + trace_id="cdef-1234-5678-90ab", + ) + """ + + id: typing.Optional[str] = None + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + value: CreateScoreValue = pydantic.Field() + """ + The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. + """ + + comment: typing.Optional[str] = None + metadata: typing.Optional[typing.Dict[str, typing.Any]] = None + environment: typing.Optional[str] = pydantic.Field(default=None) + """ + The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + """ + + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = pydantic.Field(default=None) + """ + The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + """ + + data_type: typing_extensions.Annotated[ + typing.Optional[ScoreDataType], FieldMetadata(alias="dataType") + ] = pydantic.Field(default=None) + """ + The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. + """ + + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = pydantic.Field(default=None) + """ + Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. + """ + + source: typing.Optional[CreateScoreSource] = pydantic.Field(default=None) + """ + The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/legacy/score_v1/types/create_score_response.py b/langfuse/api/legacy/score_v1/types/create_score_response.py new file mode 100644 index 000000000..ff1d27c2c --- /dev/null +++ b/langfuse/api/legacy/score_v1/types/create_score_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel + + +class CreateScoreResponse(UniversalBaseModel): + id: str = pydantic.Field() + """ + The id of the created object in Langfuse + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/legacy/score_v1/types/create_score_source.py b/langfuse/api/legacy/score_v1/types/create_score_source.py new file mode 100644 index 000000000..7364efd61 --- /dev/null +++ b/langfuse/api/legacy/score_v1/types/create_score_source.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class CreateScoreSource(enum.StrEnum): + """ + Source values accepted when creating a score via the public REST API. + EVAL is reserved for internal evaluator outputs and is intentionally not + exposed here — use commons.ScoreSource when reading scores. + """ + + API = "API" + ANNOTATION = "ANNOTATION" + + def visit( + self, + api: typing.Callable[[], T_Result], + annotation: typing.Callable[[], T_Result], + ) -> T_Result: + if self is CreateScoreSource.API: + return api() + if self is CreateScoreSource.ANNOTATION: + return annotation() diff --git a/langfuse/api/llm_connections/__init__.py b/langfuse/api/llm_connections/__init__.py new file mode 100644 index 000000000..e4edb011c --- /dev/null +++ b/langfuse/api/llm_connections/__init__.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + DeleteLlmConnectionResponse, + LlmAdapter, + LlmConnection, + PaginatedLlmConnections, + UpsertLlmConnectionRequest, + ) +_dynamic_imports: typing.Dict[str, str] = { + "DeleteLlmConnectionResponse": ".types", + "LlmAdapter": ".types", + "LlmConnection": ".types", + "PaginatedLlmConnections": ".types", + "UpsertLlmConnectionRequest": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "DeleteLlmConnectionResponse", + "LlmAdapter", + "LlmConnection", + "PaginatedLlmConnections", + "UpsertLlmConnectionRequest", +] diff --git a/langfuse/api/llm_connections/client.py b/langfuse/api/llm_connections/client.py new file mode 100644 index 000000000..3d4e60d00 --- /dev/null +++ b/langfuse/api/llm_connections/client.py @@ -0,0 +1,392 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawLlmConnectionsClient, RawLlmConnectionsClient +from .types.delete_llm_connection_response import DeleteLlmConnectionResponse +from .types.llm_adapter import LlmAdapter +from .types.llm_connection import LlmConnection +from .types.paginated_llm_connections import PaginatedLlmConnections + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class LlmConnectionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawLlmConnectionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawLlmConnectionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawLlmConnectionsClient + """ + return self._raw_client + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedLlmConnections: + """ + Get all LLM connections in a project + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedLlmConnections + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.llm_connections.list() + """ + _response = self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + def upsert( + self, + *, + provider: str, + adapter: LlmAdapter, + secret_key: str, + base_url: typing.Optional[str] = OMIT, + custom_models: typing.Optional[typing.Sequence[str]] = OMIT, + with_default_models: typing.Optional[bool] = OMIT, + extra_headers: typing.Optional[typing.Dict[str, str]] = OMIT, + config: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> LlmConnection: + """ + Create or update an LLM connection. The connection is upserted on provider. + + Parameters + ---------- + provider : str + Provider name (e.g., 'openai', 'my-gateway'). Must be unique in project, used for upserting. + + adapter : LlmAdapter + The adapter used to interface with the LLM + + secret_key : str + Secret key for the LLM API. + + base_url : typing.Optional[str] + Custom base URL for the LLM API + + custom_models : typing.Optional[typing.Sequence[str]] + List of custom model names + + with_default_models : typing.Optional[bool] + Whether to include default models. Default is true. + + extra_headers : typing.Optional[typing.Dict[str, str]] + Extra headers to send with requests + + config : typing.Optional[typing.Dict[str, typing.Any]] + Adapter-specific configuration. Validation rules: - **Bedrock**: Required. Must be `{"region": ""}` (e.g., `{"region":"us-east-1"}`) - **OpenAI**: Optional. If provided, must be `{"useResponsesApi": }` to control whether Langfuse routes calls through OpenAI's Responses API. - **VertexAI**: Optional. If provided, must be `{"location": ""}` (e.g., `{"location":"us-central1"}`) - **Other adapters**: Not supported. Omit this field or set to null. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + LlmConnection + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.llm_connections import LlmAdapter + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.llm_connections.upsert( + provider="provider", + adapter=LlmAdapter.ANTHROPIC, + secret_key="secretKey", + ) + """ + _response = self._raw_client.upsert( + provider=provider, + adapter=adapter, + secret_key=secret_key, + base_url=base_url, + custom_models=custom_models, + with_default_models=with_default_models, + extra_headers=extra_headers, + config=config, + request_options=request_options, + ) + return _response.data + + def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteLlmConnectionResponse: + """ + Delete an LLM connection by id. Evaluators that depend on the deleted connection are automatically paused. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteLlmConnectionResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.llm_connections.delete( + id="id", + ) + """ + _response = self._raw_client.delete(id, request_options=request_options) + return _response.data + + +class AsyncLlmConnectionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawLlmConnectionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawLlmConnectionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawLlmConnectionsClient + """ + return self._raw_client + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedLlmConnections: + """ + Get all LLM connections in a project + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedLlmConnections + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.llm_connections.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def upsert( + self, + *, + provider: str, + adapter: LlmAdapter, + secret_key: str, + base_url: typing.Optional[str] = OMIT, + custom_models: typing.Optional[typing.Sequence[str]] = OMIT, + with_default_models: typing.Optional[bool] = OMIT, + extra_headers: typing.Optional[typing.Dict[str, str]] = OMIT, + config: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> LlmConnection: + """ + Create or update an LLM connection. The connection is upserted on provider. + + Parameters + ---------- + provider : str + Provider name (e.g., 'openai', 'my-gateway'). Must be unique in project, used for upserting. + + adapter : LlmAdapter + The adapter used to interface with the LLM + + secret_key : str + Secret key for the LLM API. + + base_url : typing.Optional[str] + Custom base URL for the LLM API + + custom_models : typing.Optional[typing.Sequence[str]] + List of custom model names + + with_default_models : typing.Optional[bool] + Whether to include default models. Default is true. + + extra_headers : typing.Optional[typing.Dict[str, str]] + Extra headers to send with requests + + config : typing.Optional[typing.Dict[str, typing.Any]] + Adapter-specific configuration. Validation rules: - **Bedrock**: Required. Must be `{"region": ""}` (e.g., `{"region":"us-east-1"}`) - **OpenAI**: Optional. If provided, must be `{"useResponsesApi": }` to control whether Langfuse routes calls through OpenAI's Responses API. - **VertexAI**: Optional. If provided, must be `{"location": ""}` (e.g., `{"location":"us-central1"}`) - **Other adapters**: Not supported. Omit this field or set to null. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + LlmConnection + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.llm_connections import LlmAdapter + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.llm_connections.upsert( + provider="provider", + adapter=LlmAdapter.ANTHROPIC, + secret_key="secretKey", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.upsert( + provider=provider, + adapter=adapter, + secret_key=secret_key, + base_url=base_url, + custom_models=custom_models, + with_default_models=with_default_models, + extra_headers=extra_headers, + config=config, + request_options=request_options, + ) + return _response.data + + async def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteLlmConnectionResponse: + """ + Delete an LLM connection by id. Evaluators that depend on the deleted connection are automatically paused. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteLlmConnectionResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.llm_connections.delete( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(id, request_options=request_options) + return _response.data diff --git a/langfuse/api/llm_connections/raw_client.py b/langfuse/api/llm_connections/raw_client.py new file mode 100644 index 000000000..9a6388658 --- /dev/null +++ b/langfuse/api/llm_connections/raw_client.py @@ -0,0 +1,743 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.delete_llm_connection_response import DeleteLlmConnectionResponse +from .types.llm_adapter import LlmAdapter +from .types.llm_connection import LlmConnection +from .types.paginated_llm_connections import PaginatedLlmConnections + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawLlmConnectionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PaginatedLlmConnections]: + """ + Get all LLM connections in a project + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedLlmConnections] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/llm-connections", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedLlmConnections, + parse_obj_as( + type_=PaginatedLlmConnections, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def upsert( + self, + *, + provider: str, + adapter: LlmAdapter, + secret_key: str, + base_url: typing.Optional[str] = OMIT, + custom_models: typing.Optional[typing.Sequence[str]] = OMIT, + with_default_models: typing.Optional[bool] = OMIT, + extra_headers: typing.Optional[typing.Dict[str, str]] = OMIT, + config: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[LlmConnection]: + """ + Create or update an LLM connection. The connection is upserted on provider. + + Parameters + ---------- + provider : str + Provider name (e.g., 'openai', 'my-gateway'). Must be unique in project, used for upserting. + + adapter : LlmAdapter + The adapter used to interface with the LLM + + secret_key : str + Secret key for the LLM API. + + base_url : typing.Optional[str] + Custom base URL for the LLM API + + custom_models : typing.Optional[typing.Sequence[str]] + List of custom model names + + with_default_models : typing.Optional[bool] + Whether to include default models. Default is true. + + extra_headers : typing.Optional[typing.Dict[str, str]] + Extra headers to send with requests + + config : typing.Optional[typing.Dict[str, typing.Any]] + Adapter-specific configuration. Validation rules: - **Bedrock**: Required. Must be `{"region": ""}` (e.g., `{"region":"us-east-1"}`) - **OpenAI**: Optional. If provided, must be `{"useResponsesApi": }` to control whether Langfuse routes calls through OpenAI's Responses API. - **VertexAI**: Optional. If provided, must be `{"location": ""}` (e.g., `{"location":"us-central1"}`) - **Other adapters**: Not supported. Omit this field or set to null. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[LlmConnection] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/llm-connections", + method="PUT", + json={ + "provider": provider, + "adapter": adapter, + "secretKey": secret_key, + "baseURL": base_url, + "customModels": custom_models, + "withDefaultModels": with_default_models, + "extraHeaders": extra_headers, + "config": config, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + LlmConnection, + parse_obj_as( + type_=LlmConnection, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[DeleteLlmConnectionResponse]: + """ + Delete an LLM connection by id. Evaluators that depend on the deleted connection are automatically paused. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteLlmConnectionResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/llm-connections/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteLlmConnectionResponse, + parse_obj_as( + type_=DeleteLlmConnectionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawLlmConnectionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PaginatedLlmConnections]: + """ + Get all LLM connections in a project + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedLlmConnections] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/llm-connections", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedLlmConnections, + parse_obj_as( + type_=PaginatedLlmConnections, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def upsert( + self, + *, + provider: str, + adapter: LlmAdapter, + secret_key: str, + base_url: typing.Optional[str] = OMIT, + custom_models: typing.Optional[typing.Sequence[str]] = OMIT, + with_default_models: typing.Optional[bool] = OMIT, + extra_headers: typing.Optional[typing.Dict[str, str]] = OMIT, + config: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[LlmConnection]: + """ + Create or update an LLM connection. The connection is upserted on provider. + + Parameters + ---------- + provider : str + Provider name (e.g., 'openai', 'my-gateway'). Must be unique in project, used for upserting. + + adapter : LlmAdapter + The adapter used to interface with the LLM + + secret_key : str + Secret key for the LLM API. + + base_url : typing.Optional[str] + Custom base URL for the LLM API + + custom_models : typing.Optional[typing.Sequence[str]] + List of custom model names + + with_default_models : typing.Optional[bool] + Whether to include default models. Default is true. + + extra_headers : typing.Optional[typing.Dict[str, str]] + Extra headers to send with requests + + config : typing.Optional[typing.Dict[str, typing.Any]] + Adapter-specific configuration. Validation rules: - **Bedrock**: Required. Must be `{"region": ""}` (e.g., `{"region":"us-east-1"}`) - **OpenAI**: Optional. If provided, must be `{"useResponsesApi": }` to control whether Langfuse routes calls through OpenAI's Responses API. - **VertexAI**: Optional. If provided, must be `{"location": ""}` (e.g., `{"location":"us-central1"}`) - **Other adapters**: Not supported. Omit this field or set to null. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[LlmConnection] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/llm-connections", + method="PUT", + json={ + "provider": provider, + "adapter": adapter, + "secretKey": secret_key, + "baseURL": base_url, + "customModels": custom_models, + "withDefaultModels": with_default_models, + "extraHeaders": extra_headers, + "config": config, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + LlmConnection, + parse_obj_as( + type_=LlmConnection, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[DeleteLlmConnectionResponse]: + """ + Delete an LLM connection by id. Evaluators that depend on the deleted connection are automatically paused. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteLlmConnectionResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/llm-connections/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteLlmConnectionResponse, + parse_obj_as( + type_=DeleteLlmConnectionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/llm_connections/types/__init__.py b/langfuse/api/llm_connections/types/__init__.py new file mode 100644 index 000000000..ab24fc400 --- /dev/null +++ b/langfuse/api/llm_connections/types/__init__.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .delete_llm_connection_response import DeleteLlmConnectionResponse + from .llm_adapter import LlmAdapter + from .llm_connection import LlmConnection + from .paginated_llm_connections import PaginatedLlmConnections + from .upsert_llm_connection_request import UpsertLlmConnectionRequest +_dynamic_imports: typing.Dict[str, str] = { + "DeleteLlmConnectionResponse": ".delete_llm_connection_response", + "LlmAdapter": ".llm_adapter", + "LlmConnection": ".llm_connection", + "PaginatedLlmConnections": ".paginated_llm_connections", + "UpsertLlmConnectionRequest": ".upsert_llm_connection_request", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "DeleteLlmConnectionResponse", + "LlmAdapter", + "LlmConnection", + "PaginatedLlmConnections", + "UpsertLlmConnectionRequest", +] diff --git a/langfuse/api/llm_connections/types/delete_llm_connection_response.py b/langfuse/api/llm_connections/types/delete_llm_connection_response.py new file mode 100644 index 000000000..080a1904c --- /dev/null +++ b/langfuse/api/llm_connections/types/delete_llm_connection_response.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class DeleteLlmConnectionResponse(UniversalBaseModel): + message: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/llm_connections/types/llm_adapter.py b/langfuse/api/llm_connections/types/llm_adapter.py new file mode 100644 index 000000000..08cab5cb9 --- /dev/null +++ b/langfuse/api/llm_connections/types/llm_adapter.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class LlmAdapter(enum.StrEnum): + ANTHROPIC = "anthropic" + OPEN_AI = "openai" + AZURE = "azure" + BEDROCK = "bedrock" + GOOGLE_VERTEX_AI = "google-vertex-ai" + GOOGLE_AI_STUDIO = "google-ai-studio" + + def visit( + self, + anthropic: typing.Callable[[], T_Result], + open_ai: typing.Callable[[], T_Result], + azure: typing.Callable[[], T_Result], + bedrock: typing.Callable[[], T_Result], + google_vertex_ai: typing.Callable[[], T_Result], + google_ai_studio: typing.Callable[[], T_Result], + ) -> T_Result: + if self is LlmAdapter.ANTHROPIC: + return anthropic() + if self is LlmAdapter.OPEN_AI: + return open_ai() + if self is LlmAdapter.AZURE: + return azure() + if self is LlmAdapter.BEDROCK: + return bedrock() + if self is LlmAdapter.GOOGLE_VERTEX_AI: + return google_vertex_ai() + if self is LlmAdapter.GOOGLE_AI_STUDIO: + return google_ai_studio() diff --git a/langfuse/api/llm_connections/types/llm_connection.py b/langfuse/api/llm_connections/types/llm_connection.py new file mode 100644 index 000000000..470fd3d76 --- /dev/null +++ b/langfuse/api/llm_connections/types/llm_connection.py @@ -0,0 +1,77 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class LlmConnection(UniversalBaseModel): + """ + LLM API connection configuration (secrets excluded) + """ + + id: str + provider: str = pydantic.Field() + """ + Provider name (e.g., 'openai', 'my-gateway'). Must be unique in project, used for upserting. + """ + + adapter: str = pydantic.Field() + """ + The adapter used to interface with the LLM + """ + + display_secret_key: typing_extensions.Annotated[ + str, FieldMetadata(alias="displaySecretKey") + ] = pydantic.Field() + """ + Masked version of the secret key for display purposes + """ + + base_url: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="baseURL") + ] = pydantic.Field(default=None) + """ + Custom base URL for the LLM API + """ + + custom_models: typing_extensions.Annotated[ + typing.List[str], FieldMetadata(alias="customModels") + ] = pydantic.Field() + """ + List of custom model names available for this connection + """ + + with_default_models: typing_extensions.Annotated[ + bool, FieldMetadata(alias="withDefaultModels") + ] = pydantic.Field() + """ + Whether to include default models for this adapter + """ + + extra_header_keys: typing_extensions.Annotated[ + typing.List[str], FieldMetadata(alias="extraHeaderKeys") + ] = pydantic.Field() + """ + Keys of extra headers sent with requests (values excluded for security) + """ + + config: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Adapter-specific configuration. Required for Bedrock (`{"region":"us-east-1"}`), optional for OpenAI (`{"useResponsesApi":true}`), optional for VertexAI (`{"location":"us-central1"}`), not used by other adapters. + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/llm_connections/types/paginated_llm_connections.py b/langfuse/api/llm_connections/types/paginated_llm_connections.py new file mode 100644 index 000000000..5d8ce52af --- /dev/null +++ b/langfuse/api/llm_connections/types/paginated_llm_connections.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse +from .llm_connection import LlmConnection + + +class PaginatedLlmConnections(UniversalBaseModel): + data: typing.List[LlmConnection] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/llm_connections/types/upsert_llm_connection_request.py b/langfuse/api/llm_connections/types/upsert_llm_connection_request.py new file mode 100644 index 000000000..4d99f183a --- /dev/null +++ b/langfuse/api/llm_connections/types/upsert_llm_connection_request.py @@ -0,0 +1,69 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .llm_adapter import LlmAdapter + + +class UpsertLlmConnectionRequest(UniversalBaseModel): + """ + Request to create or update an LLM connection (upsert) + """ + + provider: str = pydantic.Field() + """ + Provider name (e.g., 'openai', 'my-gateway'). Must be unique in project, used for upserting. + """ + + adapter: LlmAdapter = pydantic.Field() + """ + The adapter used to interface with the LLM + """ + + secret_key: typing_extensions.Annotated[str, FieldMetadata(alias="secretKey")] = ( + pydantic.Field() + ) + """ + Secret key for the LLM API. + """ + + base_url: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="baseURL") + ] = pydantic.Field(default=None) + """ + Custom base URL for the LLM API + """ + + custom_models: typing_extensions.Annotated[ + typing.Optional[typing.List[str]], FieldMetadata(alias="customModels") + ] = pydantic.Field(default=None) + """ + List of custom model names + """ + + with_default_models: typing_extensions.Annotated[ + typing.Optional[bool], FieldMetadata(alias="withDefaultModels") + ] = pydantic.Field(default=None) + """ + Whether to include default models. Default is true. + """ + + extra_headers: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, str]], FieldMetadata(alias="extraHeaders") + ] = pydantic.Field(default=None) + """ + Extra headers to send with requests + """ + + config: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Adapter-specific configuration. Validation rules: - **Bedrock**: Required. Must be `{"region": ""}` (e.g., `{"region":"us-east-1"}`) - **OpenAI**: Optional. If provided, must be `{"useResponsesApi": }` to control whether Langfuse routes calls through OpenAI's Responses API. - **VertexAI**: Optional. If provided, must be `{"location": ""}` (e.g., `{"location":"us-central1"}`) - **Other adapters**: Not supported. Omit this field or set to null. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/media/__init__.py b/langfuse/api/media/__init__.py new file mode 100644 index 000000000..85d8f7b4f --- /dev/null +++ b/langfuse/api/media/__init__.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + GetMediaResponse, + GetMediaUploadUrlRequest, + GetMediaUploadUrlResponse, + MediaContentType, + PatchMediaBody, + ) +_dynamic_imports: typing.Dict[str, str] = { + "GetMediaResponse": ".types", + "GetMediaUploadUrlRequest": ".types", + "GetMediaUploadUrlResponse": ".types", + "MediaContentType": ".types", + "PatchMediaBody": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "GetMediaResponse", + "GetMediaUploadUrlRequest", + "GetMediaUploadUrlResponse", + "MediaContentType", + "PatchMediaBody", +] diff --git a/langfuse/api/media/client.py b/langfuse/api/media/client.py new file mode 100644 index 000000000..b22272b92 --- /dev/null +++ b/langfuse/api/media/client.py @@ -0,0 +1,427 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawMediaClient, RawMediaClient +from .types.get_media_response import GetMediaResponse +from .types.get_media_upload_url_response import GetMediaUploadUrlResponse +from .types.media_content_type import MediaContentType + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class MediaClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawMediaClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawMediaClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawMediaClient + """ + return self._raw_client + + def get( + self, media_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> GetMediaResponse: + """ + Get a media record + + Parameters + ---------- + media_id : str + The unique langfuse identifier of a media record + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMediaResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.media.get( + media_id="mediaId", + ) + """ + _response = self._raw_client.get(media_id, request_options=request_options) + return _response.data + + def patch( + self, + media_id: str, + *, + uploaded_at: dt.datetime, + upload_http_status: int, + upload_http_error: typing.Optional[str] = OMIT, + upload_time_ms: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> None: + """ + Patch a media record + + Parameters + ---------- + media_id : str + The unique langfuse identifier of a media record + + uploaded_at : dt.datetime + The date and time when the media record was uploaded + + upload_http_status : int + The HTTP status code of the upload + + upload_http_error : typing.Optional[str] + The HTTP error message of the upload + + upload_time_ms : typing.Optional[int] + The time in milliseconds it took to upload the media record + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import datetime + + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.media.patch( + media_id="mediaId", + uploaded_at=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + upload_http_status=1, + ) + """ + _response = self._raw_client.patch( + media_id, + uploaded_at=uploaded_at, + upload_http_status=upload_http_status, + upload_http_error=upload_http_error, + upload_time_ms=upload_time_ms, + request_options=request_options, + ) + return _response.data + + def get_upload_url( + self, + *, + trace_id: str, + content_type: MediaContentType, + content_length: int, + sha256hash: str, + field: str, + observation_id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetMediaUploadUrlResponse: + """ + Get a presigned upload URL for a media record + + Parameters + ---------- + trace_id : str + The trace ID associated with the media record + + content_type : MediaContentType + + content_length : int + The size of the media record in bytes + + sha256hash : str + The SHA-256 hash of the media record + + field : str + The trace / observation field the media record is associated with. This can be one of `input`, `output`, `metadata` + + observation_id : typing.Optional[str] + The observation ID associated with the media record. If the media record is associated directly with a trace, this will be null. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMediaUploadUrlResponse + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.media import MediaContentType + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.media.get_upload_url( + trace_id="traceId", + content_type=MediaContentType.IMAGE_PNG, + content_length=1, + sha256hash="sha256Hash", + field="field", + ) + """ + _response = self._raw_client.get_upload_url( + trace_id=trace_id, + content_type=content_type, + content_length=content_length, + sha256hash=sha256hash, + field=field, + observation_id=observation_id, + request_options=request_options, + ) + return _response.data + + +class AsyncMediaClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawMediaClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawMediaClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawMediaClient + """ + return self._raw_client + + async def get( + self, media_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> GetMediaResponse: + """ + Get a media record + + Parameters + ---------- + media_id : str + The unique langfuse identifier of a media record + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMediaResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.media.get( + media_id="mediaId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + media_id, request_options=request_options + ) + return _response.data + + async def patch( + self, + media_id: str, + *, + uploaded_at: dt.datetime, + upload_http_status: int, + upload_http_error: typing.Optional[str] = OMIT, + upload_time_ms: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> None: + """ + Patch a media record + + Parameters + ---------- + media_id : str + The unique langfuse identifier of a media record + + uploaded_at : dt.datetime + The date and time when the media record was uploaded + + upload_http_status : int + The HTTP status code of the upload + + upload_http_error : typing.Optional[str] + The HTTP error message of the upload + + upload_time_ms : typing.Optional[int] + The time in milliseconds it took to upload the media record + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + import datetime + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.media.patch( + media_id="mediaId", + uploaded_at=datetime.datetime.fromisoformat( + "2024-01-15 09:30:00+00:00", + ), + upload_http_status=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.patch( + media_id, + uploaded_at=uploaded_at, + upload_http_status=upload_http_status, + upload_http_error=upload_http_error, + upload_time_ms=upload_time_ms, + request_options=request_options, + ) + return _response.data + + async def get_upload_url( + self, + *, + trace_id: str, + content_type: MediaContentType, + content_length: int, + sha256hash: str, + field: str, + observation_id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetMediaUploadUrlResponse: + """ + Get a presigned upload URL for a media record + + Parameters + ---------- + trace_id : str + The trace ID associated with the media record + + content_type : MediaContentType + + content_length : int + The size of the media record in bytes + + sha256hash : str + The SHA-256 hash of the media record + + field : str + The trace / observation field the media record is associated with. This can be one of `input`, `output`, `metadata` + + observation_id : typing.Optional[str] + The observation ID associated with the media record. If the media record is associated directly with a trace, this will be null. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetMediaUploadUrlResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.media import MediaContentType + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.media.get_upload_url( + trace_id="traceId", + content_type=MediaContentType.IMAGE_PNG, + content_length=1, + sha256hash="sha256Hash", + field="field", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_upload_url( + trace_id=trace_id, + content_type=content_type, + content_length=content_length, + sha256hash=sha256hash, + field=field, + observation_id=observation_id, + request_options=request_options, + ) + return _response.data diff --git a/langfuse/api/media/raw_client.py b/langfuse/api/media/raw_client.py new file mode 100644 index 000000000..4cc619770 --- /dev/null +++ b/langfuse/api/media/raw_client.py @@ -0,0 +1,739 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.get_media_response import GetMediaResponse +from .types.get_media_upload_url_response import GetMediaUploadUrlResponse +from .types.media_content_type import MediaContentType + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawMediaClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get( + self, media_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetMediaResponse]: + """ + Get a media record + + Parameters + ---------- + media_id : str + The unique langfuse identifier of a media record + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetMediaResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/media/{jsonable_encoder(media_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMediaResponse, + parse_obj_as( + type_=GetMediaResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def patch( + self, + media_id: str, + *, + uploaded_at: dt.datetime, + upload_http_status: int, + upload_http_error: typing.Optional[str] = OMIT, + upload_time_ms: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[None]: + """ + Patch a media record + + Parameters + ---------- + media_id : str + The unique langfuse identifier of a media record + + uploaded_at : dt.datetime + The date and time when the media record was uploaded + + upload_http_status : int + The HTTP status code of the upload + + upload_http_error : typing.Optional[str] + The HTTP error message of the upload + + upload_time_ms : typing.Optional[int] + The time in milliseconds it took to upload the media record + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/media/{jsonable_encoder(media_id)}", + method="PATCH", + json={ + "uploadedAt": uploaded_at, + "uploadHttpStatus": upload_http_status, + "uploadHttpError": upload_http_error, + "uploadTimeMs": upload_time_ms, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_upload_url( + self, + *, + trace_id: str, + content_type: MediaContentType, + content_length: int, + sha256hash: str, + field: str, + observation_id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetMediaUploadUrlResponse]: + """ + Get a presigned upload URL for a media record + + Parameters + ---------- + trace_id : str + The trace ID associated with the media record + + content_type : MediaContentType + + content_length : int + The size of the media record in bytes + + sha256hash : str + The SHA-256 hash of the media record + + field : str + The trace / observation field the media record is associated with. This can be one of `input`, `output`, `metadata` + + observation_id : typing.Optional[str] + The observation ID associated with the media record. If the media record is associated directly with a trace, this will be null. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetMediaUploadUrlResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/media", + method="POST", + json={ + "traceId": trace_id, + "observationId": observation_id, + "contentType": content_type, + "contentLength": content_length, + "sha256Hash": sha256hash, + "field": field, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMediaUploadUrlResponse, + parse_obj_as( + type_=GetMediaUploadUrlResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawMediaClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get( + self, media_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetMediaResponse]: + """ + Get a media record + + Parameters + ---------- + media_id : str + The unique langfuse identifier of a media record + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetMediaResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/media/{jsonable_encoder(media_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMediaResponse, + parse_obj_as( + type_=GetMediaResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def patch( + self, + media_id: str, + *, + uploaded_at: dt.datetime, + upload_http_status: int, + upload_http_error: typing.Optional[str] = OMIT, + upload_time_ms: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[None]: + """ + Patch a media record + + Parameters + ---------- + media_id : str + The unique langfuse identifier of a media record + + uploaded_at : dt.datetime + The date and time when the media record was uploaded + + upload_http_status : int + The HTTP status code of the upload + + upload_http_error : typing.Optional[str] + The HTTP error message of the upload + + upload_time_ms : typing.Optional[int] + The time in milliseconds it took to upload the media record + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/media/{jsonable_encoder(media_id)}", + method="PATCH", + json={ + "uploadedAt": uploaded_at, + "uploadHttpStatus": upload_http_status, + "uploadHttpError": upload_http_error, + "uploadTimeMs": upload_time_ms, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_upload_url( + self, + *, + trace_id: str, + content_type: MediaContentType, + content_length: int, + sha256hash: str, + field: str, + observation_id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetMediaUploadUrlResponse]: + """ + Get a presigned upload URL for a media record + + Parameters + ---------- + trace_id : str + The trace ID associated with the media record + + content_type : MediaContentType + + content_length : int + The size of the media record in bytes + + sha256hash : str + The SHA-256 hash of the media record + + field : str + The trace / observation field the media record is associated with. This can be one of `input`, `output`, `metadata` + + observation_id : typing.Optional[str] + The observation ID associated with the media record. If the media record is associated directly with a trace, this will be null. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetMediaUploadUrlResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/media", + method="POST", + json={ + "traceId": trace_id, + "observationId": observation_id, + "contentType": content_type, + "contentLength": content_length, + "sha256Hash": sha256hash, + "field": field, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetMediaUploadUrlResponse, + parse_obj_as( + type_=GetMediaUploadUrlResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/media/types/__init__.py b/langfuse/api/media/types/__init__.py new file mode 100644 index 000000000..0fb9a44ed --- /dev/null +++ b/langfuse/api/media/types/__init__.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .get_media_response import GetMediaResponse + from .get_media_upload_url_request import GetMediaUploadUrlRequest + from .get_media_upload_url_response import GetMediaUploadUrlResponse + from .media_content_type import MediaContentType + from .patch_media_body import PatchMediaBody +_dynamic_imports: typing.Dict[str, str] = { + "GetMediaResponse": ".get_media_response", + "GetMediaUploadUrlRequest": ".get_media_upload_url_request", + "GetMediaUploadUrlResponse": ".get_media_upload_url_response", + "MediaContentType": ".media_content_type", + "PatchMediaBody": ".patch_media_body", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "GetMediaResponse", + "GetMediaUploadUrlRequest", + "GetMediaUploadUrlResponse", + "MediaContentType", + "PatchMediaBody", +] diff --git a/langfuse/api/media/types/get_media_response.py b/langfuse/api/media/types/get_media_response.py new file mode 100644 index 000000000..fc1f70329 --- /dev/null +++ b/langfuse/api/media/types/get_media_response.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class GetMediaResponse(UniversalBaseModel): + media_id: typing_extensions.Annotated[str, FieldMetadata(alias="mediaId")] = ( + pydantic.Field() + ) + """ + The unique langfuse identifier of a media record + """ + + content_type: typing_extensions.Annotated[ + str, FieldMetadata(alias="contentType") + ] = pydantic.Field() + """ + The MIME type of the media record + """ + + content_length: typing_extensions.Annotated[ + int, FieldMetadata(alias="contentLength") + ] = pydantic.Field() + """ + The size of the media record in bytes + """ + + uploaded_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="uploadedAt") + ] = pydantic.Field() + """ + The date and time when the media record was uploaded + """ + + url: str = pydantic.Field() + """ + The download URL of the media record + """ + + url_expiry: typing_extensions.Annotated[str, FieldMetadata(alias="urlExpiry")] = ( + pydantic.Field() + ) + """ + The expiry date and time of the media record download URL + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/media/types/get_media_upload_url_request.py b/langfuse/api/media/types/get_media_upload_url_request.py new file mode 100644 index 000000000..99f055847 --- /dev/null +++ b/langfuse/api/media/types/get_media_upload_url_request.py @@ -0,0 +1,51 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .media_content_type import MediaContentType + + +class GetMediaUploadUrlRequest(UniversalBaseModel): + trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] = ( + pydantic.Field() + ) + """ + The trace ID associated with the media record + """ + + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = pydantic.Field(default=None) + """ + The observation ID associated with the media record. If the media record is associated directly with a trace, this will be null. + """ + + content_type: typing_extensions.Annotated[ + MediaContentType, FieldMetadata(alias="contentType") + ] + content_length: typing_extensions.Annotated[ + int, FieldMetadata(alias="contentLength") + ] = pydantic.Field() + """ + The size of the media record in bytes + """ + + sha256hash: typing_extensions.Annotated[str, FieldMetadata(alias="sha256Hash")] = ( + pydantic.Field() + ) + """ + The SHA-256 hash of the media record + """ + + field: str = pydantic.Field() + """ + The trace / observation field the media record is associated with. This can be one of `input`, `output`, `metadata` + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/media/types/get_media_upload_url_response.py b/langfuse/api/media/types/get_media_upload_url_response.py new file mode 100644 index 000000000..90c735be3 --- /dev/null +++ b/langfuse/api/media/types/get_media_upload_url_response.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class GetMediaUploadUrlResponse(UniversalBaseModel): + upload_url: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="uploadUrl") + ] = pydantic.Field(default=None) + """ + The presigned upload URL. If the asset is already uploaded, this will be null + """ + + media_id: typing_extensions.Annotated[str, FieldMetadata(alias="mediaId")] = ( + pydantic.Field() + ) + """ + The unique langfuse identifier of a media record + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/media/types/media_content_type.py b/langfuse/api/media/types/media_content_type.py new file mode 100644 index 000000000..9fb5507bc --- /dev/null +++ b/langfuse/api/media/types/media_content_type.py @@ -0,0 +1,232 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class MediaContentType(enum.StrEnum): + """ + The MIME type of the media record + """ + + IMAGE_PNG = "image/png" + IMAGE_JPEG = "image/jpeg" + IMAGE_JPG = "image/jpg" + IMAGE_WEBP = "image/webp" + IMAGE_GIF = "image/gif" + IMAGE_SVG_XML = "image/svg+xml" + IMAGE_TIFF = "image/tiff" + IMAGE_BMP = "image/bmp" + IMAGE_AVIF = "image/avif" + IMAGE_HEIC = "image/heic" + AUDIO_MPEG = "audio/mpeg" + AUDIO_MP3 = "audio/mp3" + AUDIO_WAV = "audio/wav" + AUDIO_OGG = "audio/ogg" + AUDIO_OGA = "audio/oga" + AUDIO_AAC = "audio/aac" + AUDIO_MP4 = "audio/mp4" + AUDIO_FLAC = "audio/flac" + AUDIO_OPUS = "audio/opus" + AUDIO_WEBM = "audio/webm" + VIDEO_MP4 = "video/mp4" + VIDEO_WEBM = "video/webm" + VIDEO_OGG = "video/ogg" + VIDEO_MPEG = "video/mpeg" + VIDEO_QUICKTIME = "video/quicktime" + VIDEO_X_MSVIDEO = "video/x-msvideo" + VIDEO_X_MATROSKA = "video/x-matroska" + TEXT_PLAIN = "text/plain" + TEXT_HTML = "text/html" + TEXT_CSS = "text/css" + TEXT_CSV = "text/csv" + TEXT_MARKDOWN = "text/markdown" + TEXT_X_PYTHON = "text/x-python" + APPLICATION_JAVASCRIPT = "application/javascript" + TEXT_X_TYPESCRIPT = "text/x-typescript" + APPLICATION_X_YAML = "application/x-yaml" + APPLICATION_PDF = "application/pdf" + APPLICATION_MSWORD = "application/msword" + APPLICATION_MS_EXCEL = "application/vnd.ms-excel" + APPLICATION_OPENXML_SPREADSHEET = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + APPLICATION_ZIP = "application/zip" + APPLICATION_JSON = "application/json" + APPLICATION_XML = "application/xml" + APPLICATION_OCTET_STREAM = "application/octet-stream" + APPLICATION_OPENXML_WORD = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) + APPLICATION_OPENXML_PRESENTATION = ( + "application/vnd.openxmlformats-officedocument.presentationml.presentation" + ) + APPLICATION_RTF = "application/rtf" + APPLICATION_X_NDJSON = "application/x-ndjson" + APPLICATION_PARQUET = "application/vnd.apache.parquet" + APPLICATION_GZIP = "application/gzip" + APPLICATION_X_TAR = "application/x-tar" + APPLICATION_X7Z_COMPRESSED = "application/x-7z-compressed" + + def visit( + self, + image_png: typing.Callable[[], T_Result], + image_jpeg: typing.Callable[[], T_Result], + image_jpg: typing.Callable[[], T_Result], + image_webp: typing.Callable[[], T_Result], + image_gif: typing.Callable[[], T_Result], + image_svg_xml: typing.Callable[[], T_Result], + image_tiff: typing.Callable[[], T_Result], + image_bmp: typing.Callable[[], T_Result], + image_avif: typing.Callable[[], T_Result], + image_heic: typing.Callable[[], T_Result], + audio_mpeg: typing.Callable[[], T_Result], + audio_mp3: typing.Callable[[], T_Result], + audio_wav: typing.Callable[[], T_Result], + audio_ogg: typing.Callable[[], T_Result], + audio_oga: typing.Callable[[], T_Result], + audio_aac: typing.Callable[[], T_Result], + audio_mp4: typing.Callable[[], T_Result], + audio_flac: typing.Callable[[], T_Result], + audio_opus: typing.Callable[[], T_Result], + audio_webm: typing.Callable[[], T_Result], + video_mp4: typing.Callable[[], T_Result], + video_webm: typing.Callable[[], T_Result], + video_ogg: typing.Callable[[], T_Result], + video_mpeg: typing.Callable[[], T_Result], + video_quicktime: typing.Callable[[], T_Result], + video_x_msvideo: typing.Callable[[], T_Result], + video_x_matroska: typing.Callable[[], T_Result], + text_plain: typing.Callable[[], T_Result], + text_html: typing.Callable[[], T_Result], + text_css: typing.Callable[[], T_Result], + text_csv: typing.Callable[[], T_Result], + text_markdown: typing.Callable[[], T_Result], + text_x_python: typing.Callable[[], T_Result], + application_javascript: typing.Callable[[], T_Result], + text_x_typescript: typing.Callable[[], T_Result], + application_x_yaml: typing.Callable[[], T_Result], + application_pdf: typing.Callable[[], T_Result], + application_msword: typing.Callable[[], T_Result], + application_ms_excel: typing.Callable[[], T_Result], + application_openxml_spreadsheet: typing.Callable[[], T_Result], + application_zip: typing.Callable[[], T_Result], + application_json: typing.Callable[[], T_Result], + application_xml: typing.Callable[[], T_Result], + application_octet_stream: typing.Callable[[], T_Result], + application_openxml_word: typing.Callable[[], T_Result], + application_openxml_presentation: typing.Callable[[], T_Result], + application_rtf: typing.Callable[[], T_Result], + application_x_ndjson: typing.Callable[[], T_Result], + application_parquet: typing.Callable[[], T_Result], + application_gzip: typing.Callable[[], T_Result], + application_x_tar: typing.Callable[[], T_Result], + application_x7z_compressed: typing.Callable[[], T_Result], + ) -> T_Result: + if self is MediaContentType.IMAGE_PNG: + return image_png() + if self is MediaContentType.IMAGE_JPEG: + return image_jpeg() + if self is MediaContentType.IMAGE_JPG: + return image_jpg() + if self is MediaContentType.IMAGE_WEBP: + return image_webp() + if self is MediaContentType.IMAGE_GIF: + return image_gif() + if self is MediaContentType.IMAGE_SVG_XML: + return image_svg_xml() + if self is MediaContentType.IMAGE_TIFF: + return image_tiff() + if self is MediaContentType.IMAGE_BMP: + return image_bmp() + if self is MediaContentType.IMAGE_AVIF: + return image_avif() + if self is MediaContentType.IMAGE_HEIC: + return image_heic() + if self is MediaContentType.AUDIO_MPEG: + return audio_mpeg() + if self is MediaContentType.AUDIO_MP3: + return audio_mp3() + if self is MediaContentType.AUDIO_WAV: + return audio_wav() + if self is MediaContentType.AUDIO_OGG: + return audio_ogg() + if self is MediaContentType.AUDIO_OGA: + return audio_oga() + if self is MediaContentType.AUDIO_AAC: + return audio_aac() + if self is MediaContentType.AUDIO_MP4: + return audio_mp4() + if self is MediaContentType.AUDIO_FLAC: + return audio_flac() + if self is MediaContentType.AUDIO_OPUS: + return audio_opus() + if self is MediaContentType.AUDIO_WEBM: + return audio_webm() + if self is MediaContentType.VIDEO_MP4: + return video_mp4() + if self is MediaContentType.VIDEO_WEBM: + return video_webm() + if self is MediaContentType.VIDEO_OGG: + return video_ogg() + if self is MediaContentType.VIDEO_MPEG: + return video_mpeg() + if self is MediaContentType.VIDEO_QUICKTIME: + return video_quicktime() + if self is MediaContentType.VIDEO_X_MSVIDEO: + return video_x_msvideo() + if self is MediaContentType.VIDEO_X_MATROSKA: + return video_x_matroska() + if self is MediaContentType.TEXT_PLAIN: + return text_plain() + if self is MediaContentType.TEXT_HTML: + return text_html() + if self is MediaContentType.TEXT_CSS: + return text_css() + if self is MediaContentType.TEXT_CSV: + return text_csv() + if self is MediaContentType.TEXT_MARKDOWN: + return text_markdown() + if self is MediaContentType.TEXT_X_PYTHON: + return text_x_python() + if self is MediaContentType.APPLICATION_JAVASCRIPT: + return application_javascript() + if self is MediaContentType.TEXT_X_TYPESCRIPT: + return text_x_typescript() + if self is MediaContentType.APPLICATION_X_YAML: + return application_x_yaml() + if self is MediaContentType.APPLICATION_PDF: + return application_pdf() + if self is MediaContentType.APPLICATION_MSWORD: + return application_msword() + if self is MediaContentType.APPLICATION_MS_EXCEL: + return application_ms_excel() + if self is MediaContentType.APPLICATION_OPENXML_SPREADSHEET: + return application_openxml_spreadsheet() + if self is MediaContentType.APPLICATION_ZIP: + return application_zip() + if self is MediaContentType.APPLICATION_JSON: + return application_json() + if self is MediaContentType.APPLICATION_XML: + return application_xml() + if self is MediaContentType.APPLICATION_OCTET_STREAM: + return application_octet_stream() + if self is MediaContentType.APPLICATION_OPENXML_WORD: + return application_openxml_word() + if self is MediaContentType.APPLICATION_OPENXML_PRESENTATION: + return application_openxml_presentation() + if self is MediaContentType.APPLICATION_RTF: + return application_rtf() + if self is MediaContentType.APPLICATION_X_NDJSON: + return application_x_ndjson() + if self is MediaContentType.APPLICATION_PARQUET: + return application_parquet() + if self is MediaContentType.APPLICATION_GZIP: + return application_gzip() + if self is MediaContentType.APPLICATION_X_TAR: + return application_x_tar() + if self is MediaContentType.APPLICATION_X7Z_COMPRESSED: + return application_x7z_compressed() diff --git a/langfuse/api/media/types/patch_media_body.py b/langfuse/api/media/types/patch_media_body.py new file mode 100644 index 000000000..e5f93f601 --- /dev/null +++ b/langfuse/api/media/types/patch_media_body.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class PatchMediaBody(UniversalBaseModel): + uploaded_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="uploadedAt") + ] = pydantic.Field() + """ + The date and time when the media record was uploaded + """ + + upload_http_status: typing_extensions.Annotated[ + int, FieldMetadata(alias="uploadHttpStatus") + ] = pydantic.Field() + """ + The HTTP status code of the upload + """ + + upload_http_error: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="uploadHttpError") + ] = pydantic.Field(default=None) + """ + The HTTP error message of the upload + """ + + upload_time_ms: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="uploadTimeMs") + ] = pydantic.Field(default=None) + """ + The time in milliseconds it took to upload the media record + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/metrics/__init__.py b/langfuse/api/metrics/__init__.py new file mode 100644 index 000000000..0421785de --- /dev/null +++ b/langfuse/api/metrics/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import MetricsV2Response +_dynamic_imports: typing.Dict[str, str] = {"MetricsV2Response": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["MetricsV2Response"] diff --git a/langfuse/api/metrics/client.py b/langfuse/api/metrics/client.py new file mode 100644 index 000000000..ed584d99b --- /dev/null +++ b/langfuse/api/metrics/client.py @@ -0,0 +1,422 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawMetricsClient, RawMetricsClient +from .types.metrics_v2response import MetricsV2Response + + +class MetricsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawMetricsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawMetricsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawMetricsClient + """ + return self._raw_client + + def metrics( + self, *, query: str, request_options: typing.Optional[RequestOptions] = None + ) -> MetricsV2Response: + """ + Get metrics from the Langfuse project using a query object. V2 endpoint with optimized performance. + + ## V2 Differences + - Supports `observations`, `scores-numeric`, and `scores-categorical` views only (traces view not supported) + - Direct access to tags and release fields on observations + - Backwards-compatible: traceName, traceRelease, traceVersion dimensions are still available on observations view + - High cardinality dimensions are not supported and will return a 400 error (see below) + + For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). + + ## Available Views + + ### observations + Query observation-level data (spans, generations, events). + + **Dimensions:** + - `environment` - Deployment environment (e.g., production, staging) + - `type` - Type of observation (SPAN, GENERATION, EVENT) + - `name` - Name of the observation + - `level` - Logging level of the observation + - `version` - Version of the observation + - `tags` - User-defined tags + - `release` - Release version + - `traceName` - Name of the parent trace (backwards-compatible) + - `traceRelease` - Release version of the parent trace (backwards-compatible, maps to release) + - `traceVersion` - Version of the parent trace (backwards-compatible, maps to version) + - `providedModelName` - Name of the model used + - `promptName` - Name of the prompt used + - `promptVersion` - Version of the prompt used + - `startTimeMonth` - Month of start_time in YYYY-MM format + + **Measures:** + - `count` - Total number of observations + - `latency` - Observation latency (milliseconds) + - `streamingLatency` - Generation latency from completion start to end (milliseconds) + - `inputTokens` - Sum of input tokens consumed + - `outputTokens` - Sum of output tokens produced + - `totalTokens` - Sum of all tokens consumed + - `outputTokensPerSecond` - Output tokens per second + - `tokensPerSecond` - Total tokens per second + - `inputCost` - Input cost (USD) + - `outputCost` - Output cost (USD) + - `totalCost` - Total cost (USD) + - `timeToFirstToken` - Time to first token (milliseconds) + - `countScores` - Number of scores attached to the observation + + ### scores-numeric + Query numeric and boolean score data. + + **Dimensions:** + - `environment` - Deployment environment + - `name` - Name of the score (e.g., accuracy, toxicity) + - `source` - Origin of the score (API, ANNOTATION, EVAL) + - `dataType` - Data type (NUMERIC, BOOLEAN) + - `configId` - Identifier of the score config + - `timestampMonth` - Month in YYYY-MM format + - `timestampDay` - Day in YYYY-MM-DD format + - `value` - Numeric value of the score + - `traceName` - Name of the parent trace + - `tags` - Tags + - `traceRelease` - Release version + - `traceVersion` - Version + - `observationName` - Name of the associated observation + - `observationModelName` - Model name of the associated observation + - `observationPromptName` - Prompt name of the associated observation + - `observationPromptVersion` - Prompt version of the associated observation + + **Measures:** + - `count` - Total number of scores + - `value` - Score value (for aggregations) + + ### scores-categorical + Query categorical score data. Same dimensions as scores-numeric except uses `stringValue` instead of `value`. + + **Measures:** + - `count` - Total number of scores + + ## High Cardinality Dimensions + The following dimensions cannot be used as grouping dimensions in v2 metrics API as they can cause performance issues. + Use them in filters instead. + + **observations view:** + - `id` - Use traceId filter to narrow down results + - `traceId` - Use traceId filter instead + - `userId` - Use userId filter instead + - `sessionId` - Use sessionId filter instead + - `parentObservationId` - Use parentObservationId filter instead + + **scores-numeric / scores-categorical views:** + - `id` - Use specific filters to narrow down results + - `traceId` - Use traceId filter instead + - `userId` - Use userId filter instead + - `sessionId` - Use sessionId filter instead + - `observationId` - Use observationId filter instead + + ## Aggregations + Available aggregation functions: `sum`, `avg`, `count`, `max`, `min`, `p50`, `p75`, `p90`, `p95`, `p99`, `histogram` + + ## Time Granularities + Available granularities for timeDimension: `auto`, `minute`, `hour`, `day`, `week`, `month` + - `auto` bins the data into approximately 50 buckets based on the time range + + Parameters + ---------- + query : str + JSON string containing the query parameters with the following structure: + ```json + { + "view": string, // Required. One of "observations", "scores-numeric", "scores-categorical" + "dimensions": [ // Optional. Default: [] + { + "field": string // Field to group by (see available dimensions above) + } + ], + "metrics": [ // Required. At least one metric must be provided + { + "measure": string, // What to measure (see available measures above) + "aggregation": string // How to aggregate: "sum", "avg", "count", "max", "min", "p50", "p75", "p90", "p95", "p99", "histogram" + } + ], + "filters": [ // Optional. Default: [] + { + "column": string, // Column to filter on (any dimension field) + "operator": string, // Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject/numberObject: same as string/number with required "key" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Value to compare against + "type": string, // Data type: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "key": string // Required only for stringObject/numberObject types (e.g., metadata filtering) + } + ], + "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time + "granularity": string // One of "auto", "minute", "hour", "day", "week", "month" + }, + "fromTimestamp": string, // Required. ISO datetime string for start of time range + "toTimestamp": string, // Required. ISO datetime string for end of time range (must be after fromTimestamp) + "orderBy": [ // Optional. Default: null + { + "field": string, // Field to order by (dimension or metric alias) + "direction": string // "asc" or "desc" + } + ], + "config": { // Optional. Query-specific configuration + "bins": number, // Optional. Number of bins for histogram aggregation (1-100), default: 10 + "row_limit": number // Optional. Maximum number of rows to return (1-1000), default: 100 + } + } + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MetricsV2Response + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.metrics.metrics( + query="query", + ) + """ + _response = self._raw_client.metrics( + query=query, request_options=request_options + ) + return _response.data + + +class AsyncMetricsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawMetricsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawMetricsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawMetricsClient + """ + return self._raw_client + + async def metrics( + self, *, query: str, request_options: typing.Optional[RequestOptions] = None + ) -> MetricsV2Response: + """ + Get metrics from the Langfuse project using a query object. V2 endpoint with optimized performance. + + ## V2 Differences + - Supports `observations`, `scores-numeric`, and `scores-categorical` views only (traces view not supported) + - Direct access to tags and release fields on observations + - Backwards-compatible: traceName, traceRelease, traceVersion dimensions are still available on observations view + - High cardinality dimensions are not supported and will return a 400 error (see below) + + For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). + + ## Available Views + + ### observations + Query observation-level data (spans, generations, events). + + **Dimensions:** + - `environment` - Deployment environment (e.g., production, staging) + - `type` - Type of observation (SPAN, GENERATION, EVENT) + - `name` - Name of the observation + - `level` - Logging level of the observation + - `version` - Version of the observation + - `tags` - User-defined tags + - `release` - Release version + - `traceName` - Name of the parent trace (backwards-compatible) + - `traceRelease` - Release version of the parent trace (backwards-compatible, maps to release) + - `traceVersion` - Version of the parent trace (backwards-compatible, maps to version) + - `providedModelName` - Name of the model used + - `promptName` - Name of the prompt used + - `promptVersion` - Version of the prompt used + - `startTimeMonth` - Month of start_time in YYYY-MM format + + **Measures:** + - `count` - Total number of observations + - `latency` - Observation latency (milliseconds) + - `streamingLatency` - Generation latency from completion start to end (milliseconds) + - `inputTokens` - Sum of input tokens consumed + - `outputTokens` - Sum of output tokens produced + - `totalTokens` - Sum of all tokens consumed + - `outputTokensPerSecond` - Output tokens per second + - `tokensPerSecond` - Total tokens per second + - `inputCost` - Input cost (USD) + - `outputCost` - Output cost (USD) + - `totalCost` - Total cost (USD) + - `timeToFirstToken` - Time to first token (milliseconds) + - `countScores` - Number of scores attached to the observation + + ### scores-numeric + Query numeric and boolean score data. + + **Dimensions:** + - `environment` - Deployment environment + - `name` - Name of the score (e.g., accuracy, toxicity) + - `source` - Origin of the score (API, ANNOTATION, EVAL) + - `dataType` - Data type (NUMERIC, BOOLEAN) + - `configId` - Identifier of the score config + - `timestampMonth` - Month in YYYY-MM format + - `timestampDay` - Day in YYYY-MM-DD format + - `value` - Numeric value of the score + - `traceName` - Name of the parent trace + - `tags` - Tags + - `traceRelease` - Release version + - `traceVersion` - Version + - `observationName` - Name of the associated observation + - `observationModelName` - Model name of the associated observation + - `observationPromptName` - Prompt name of the associated observation + - `observationPromptVersion` - Prompt version of the associated observation + + **Measures:** + - `count` - Total number of scores + - `value` - Score value (for aggregations) + + ### scores-categorical + Query categorical score data. Same dimensions as scores-numeric except uses `stringValue` instead of `value`. + + **Measures:** + - `count` - Total number of scores + + ## High Cardinality Dimensions + The following dimensions cannot be used as grouping dimensions in v2 metrics API as they can cause performance issues. + Use them in filters instead. + + **observations view:** + - `id` - Use traceId filter to narrow down results + - `traceId` - Use traceId filter instead + - `userId` - Use userId filter instead + - `sessionId` - Use sessionId filter instead + - `parentObservationId` - Use parentObservationId filter instead + + **scores-numeric / scores-categorical views:** + - `id` - Use specific filters to narrow down results + - `traceId` - Use traceId filter instead + - `userId` - Use userId filter instead + - `sessionId` - Use sessionId filter instead + - `observationId` - Use observationId filter instead + + ## Aggregations + Available aggregation functions: `sum`, `avg`, `count`, `max`, `min`, `p50`, `p75`, `p90`, `p95`, `p99`, `histogram` + + ## Time Granularities + Available granularities for timeDimension: `auto`, `minute`, `hour`, `day`, `week`, `month` + - `auto` bins the data into approximately 50 buckets based on the time range + + Parameters + ---------- + query : str + JSON string containing the query parameters with the following structure: + ```json + { + "view": string, // Required. One of "observations", "scores-numeric", "scores-categorical" + "dimensions": [ // Optional. Default: [] + { + "field": string // Field to group by (see available dimensions above) + } + ], + "metrics": [ // Required. At least one metric must be provided + { + "measure": string, // What to measure (see available measures above) + "aggregation": string // How to aggregate: "sum", "avg", "count", "max", "min", "p50", "p75", "p90", "p95", "p99", "histogram" + } + ], + "filters": [ // Optional. Default: [] + { + "column": string, // Column to filter on (any dimension field) + "operator": string, // Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject/numberObject: same as string/number with required "key" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Value to compare against + "type": string, // Data type: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "key": string // Required only for stringObject/numberObject types (e.g., metadata filtering) + } + ], + "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time + "granularity": string // One of "auto", "minute", "hour", "day", "week", "month" + }, + "fromTimestamp": string, // Required. ISO datetime string for start of time range + "toTimestamp": string, // Required. ISO datetime string for end of time range (must be after fromTimestamp) + "orderBy": [ // Optional. Default: null + { + "field": string, // Field to order by (dimension or metric alias) + "direction": string // "asc" or "desc" + } + ], + "config": { // Optional. Query-specific configuration + "bins": number, // Optional. Number of bins for histogram aggregation (1-100), default: 10 + "row_limit": number // Optional. Maximum number of rows to return (1-1000), default: 100 + } + } + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MetricsV2Response + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.metrics.metrics( + query="query", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.metrics( + query=query, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/metrics/raw_client.py b/langfuse/api/metrics/raw_client.py new file mode 100644 index 000000000..69c976bcc --- /dev/null +++ b/langfuse/api/metrics/raw_client.py @@ -0,0 +1,530 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.metrics_v2response import MetricsV2Response + + +class RawMetricsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def metrics( + self, *, query: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[MetricsV2Response]: + """ + Get metrics from the Langfuse project using a query object. V2 endpoint with optimized performance. + + ## V2 Differences + - Supports `observations`, `scores-numeric`, and `scores-categorical` views only (traces view not supported) + - Direct access to tags and release fields on observations + - Backwards-compatible: traceName, traceRelease, traceVersion dimensions are still available on observations view + - High cardinality dimensions are not supported and will return a 400 error (see below) + + For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). + + ## Available Views + + ### observations + Query observation-level data (spans, generations, events). + + **Dimensions:** + - `environment` - Deployment environment (e.g., production, staging) + - `type` - Type of observation (SPAN, GENERATION, EVENT) + - `name` - Name of the observation + - `level` - Logging level of the observation + - `version` - Version of the observation + - `tags` - User-defined tags + - `release` - Release version + - `traceName` - Name of the parent trace (backwards-compatible) + - `traceRelease` - Release version of the parent trace (backwards-compatible, maps to release) + - `traceVersion` - Version of the parent trace (backwards-compatible, maps to version) + - `providedModelName` - Name of the model used + - `promptName` - Name of the prompt used + - `promptVersion` - Version of the prompt used + - `startTimeMonth` - Month of start_time in YYYY-MM format + + **Measures:** + - `count` - Total number of observations + - `latency` - Observation latency (milliseconds) + - `streamingLatency` - Generation latency from completion start to end (milliseconds) + - `inputTokens` - Sum of input tokens consumed + - `outputTokens` - Sum of output tokens produced + - `totalTokens` - Sum of all tokens consumed + - `outputTokensPerSecond` - Output tokens per second + - `tokensPerSecond` - Total tokens per second + - `inputCost` - Input cost (USD) + - `outputCost` - Output cost (USD) + - `totalCost` - Total cost (USD) + - `timeToFirstToken` - Time to first token (milliseconds) + - `countScores` - Number of scores attached to the observation + + ### scores-numeric + Query numeric and boolean score data. + + **Dimensions:** + - `environment` - Deployment environment + - `name` - Name of the score (e.g., accuracy, toxicity) + - `source` - Origin of the score (API, ANNOTATION, EVAL) + - `dataType` - Data type (NUMERIC, BOOLEAN) + - `configId` - Identifier of the score config + - `timestampMonth` - Month in YYYY-MM format + - `timestampDay` - Day in YYYY-MM-DD format + - `value` - Numeric value of the score + - `traceName` - Name of the parent trace + - `tags` - Tags + - `traceRelease` - Release version + - `traceVersion` - Version + - `observationName` - Name of the associated observation + - `observationModelName` - Model name of the associated observation + - `observationPromptName` - Prompt name of the associated observation + - `observationPromptVersion` - Prompt version of the associated observation + + **Measures:** + - `count` - Total number of scores + - `value` - Score value (for aggregations) + + ### scores-categorical + Query categorical score data. Same dimensions as scores-numeric except uses `stringValue` instead of `value`. + + **Measures:** + - `count` - Total number of scores + + ## High Cardinality Dimensions + The following dimensions cannot be used as grouping dimensions in v2 metrics API as they can cause performance issues. + Use them in filters instead. + + **observations view:** + - `id` - Use traceId filter to narrow down results + - `traceId` - Use traceId filter instead + - `userId` - Use userId filter instead + - `sessionId` - Use sessionId filter instead + - `parentObservationId` - Use parentObservationId filter instead + + **scores-numeric / scores-categorical views:** + - `id` - Use specific filters to narrow down results + - `traceId` - Use traceId filter instead + - `userId` - Use userId filter instead + - `sessionId` - Use sessionId filter instead + - `observationId` - Use observationId filter instead + + ## Aggregations + Available aggregation functions: `sum`, `avg`, `count`, `max`, `min`, `p50`, `p75`, `p90`, `p95`, `p99`, `histogram` + + ## Time Granularities + Available granularities for timeDimension: `auto`, `minute`, `hour`, `day`, `week`, `month` + - `auto` bins the data into approximately 50 buckets based on the time range + + Parameters + ---------- + query : str + JSON string containing the query parameters with the following structure: + ```json + { + "view": string, // Required. One of "observations", "scores-numeric", "scores-categorical" + "dimensions": [ // Optional. Default: [] + { + "field": string // Field to group by (see available dimensions above) + } + ], + "metrics": [ // Required. At least one metric must be provided + { + "measure": string, // What to measure (see available measures above) + "aggregation": string // How to aggregate: "sum", "avg", "count", "max", "min", "p50", "p75", "p90", "p95", "p99", "histogram" + } + ], + "filters": [ // Optional. Default: [] + { + "column": string, // Column to filter on (any dimension field) + "operator": string, // Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject/numberObject: same as string/number with required "key" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Value to compare against + "type": string, // Data type: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "key": string // Required only for stringObject/numberObject types (e.g., metadata filtering) + } + ], + "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time + "granularity": string // One of "auto", "minute", "hour", "day", "week", "month" + }, + "fromTimestamp": string, // Required. ISO datetime string for start of time range + "toTimestamp": string, // Required. ISO datetime string for end of time range (must be after fromTimestamp) + "orderBy": [ // Optional. Default: null + { + "field": string, // Field to order by (dimension or metric alias) + "direction": string // "asc" or "desc" + } + ], + "config": { // Optional. Query-specific configuration + "bins": number, // Optional. Number of bins for histogram aggregation (1-100), default: 10 + "row_limit": number // Optional. Maximum number of rows to return (1-1000), default: 100 + } + } + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MetricsV2Response] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/v2/metrics", + method="GET", + params={ + "query": query, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MetricsV2Response, + parse_obj_as( + type_=MetricsV2Response, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawMetricsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def metrics( + self, *, query: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[MetricsV2Response]: + """ + Get metrics from the Langfuse project using a query object. V2 endpoint with optimized performance. + + ## V2 Differences + - Supports `observations`, `scores-numeric`, and `scores-categorical` views only (traces view not supported) + - Direct access to tags and release fields on observations + - Backwards-compatible: traceName, traceRelease, traceVersion dimensions are still available on observations view + - High cardinality dimensions are not supported and will return a 400 error (see below) + + For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). + + ## Available Views + + ### observations + Query observation-level data (spans, generations, events). + + **Dimensions:** + - `environment` - Deployment environment (e.g., production, staging) + - `type` - Type of observation (SPAN, GENERATION, EVENT) + - `name` - Name of the observation + - `level` - Logging level of the observation + - `version` - Version of the observation + - `tags` - User-defined tags + - `release` - Release version + - `traceName` - Name of the parent trace (backwards-compatible) + - `traceRelease` - Release version of the parent trace (backwards-compatible, maps to release) + - `traceVersion` - Version of the parent trace (backwards-compatible, maps to version) + - `providedModelName` - Name of the model used + - `promptName` - Name of the prompt used + - `promptVersion` - Version of the prompt used + - `startTimeMonth` - Month of start_time in YYYY-MM format + + **Measures:** + - `count` - Total number of observations + - `latency` - Observation latency (milliseconds) + - `streamingLatency` - Generation latency from completion start to end (milliseconds) + - `inputTokens` - Sum of input tokens consumed + - `outputTokens` - Sum of output tokens produced + - `totalTokens` - Sum of all tokens consumed + - `outputTokensPerSecond` - Output tokens per second + - `tokensPerSecond` - Total tokens per second + - `inputCost` - Input cost (USD) + - `outputCost` - Output cost (USD) + - `totalCost` - Total cost (USD) + - `timeToFirstToken` - Time to first token (milliseconds) + - `countScores` - Number of scores attached to the observation + + ### scores-numeric + Query numeric and boolean score data. + + **Dimensions:** + - `environment` - Deployment environment + - `name` - Name of the score (e.g., accuracy, toxicity) + - `source` - Origin of the score (API, ANNOTATION, EVAL) + - `dataType` - Data type (NUMERIC, BOOLEAN) + - `configId` - Identifier of the score config + - `timestampMonth` - Month in YYYY-MM format + - `timestampDay` - Day in YYYY-MM-DD format + - `value` - Numeric value of the score + - `traceName` - Name of the parent trace + - `tags` - Tags + - `traceRelease` - Release version + - `traceVersion` - Version + - `observationName` - Name of the associated observation + - `observationModelName` - Model name of the associated observation + - `observationPromptName` - Prompt name of the associated observation + - `observationPromptVersion` - Prompt version of the associated observation + + **Measures:** + - `count` - Total number of scores + - `value` - Score value (for aggregations) + + ### scores-categorical + Query categorical score data. Same dimensions as scores-numeric except uses `stringValue` instead of `value`. + + **Measures:** + - `count` - Total number of scores + + ## High Cardinality Dimensions + The following dimensions cannot be used as grouping dimensions in v2 metrics API as they can cause performance issues. + Use them in filters instead. + + **observations view:** + - `id` - Use traceId filter to narrow down results + - `traceId` - Use traceId filter instead + - `userId` - Use userId filter instead + - `sessionId` - Use sessionId filter instead + - `parentObservationId` - Use parentObservationId filter instead + + **scores-numeric / scores-categorical views:** + - `id` - Use specific filters to narrow down results + - `traceId` - Use traceId filter instead + - `userId` - Use userId filter instead + - `sessionId` - Use sessionId filter instead + - `observationId` - Use observationId filter instead + + ## Aggregations + Available aggregation functions: `sum`, `avg`, `count`, `max`, `min`, `p50`, `p75`, `p90`, `p95`, `p99`, `histogram` + + ## Time Granularities + Available granularities for timeDimension: `auto`, `minute`, `hour`, `day`, `week`, `month` + - `auto` bins the data into approximately 50 buckets based on the time range + + Parameters + ---------- + query : str + JSON string containing the query parameters with the following structure: + ```json + { + "view": string, // Required. One of "observations", "scores-numeric", "scores-categorical" + "dimensions": [ // Optional. Default: [] + { + "field": string // Field to group by (see available dimensions above) + } + ], + "metrics": [ // Required. At least one metric must be provided + { + "measure": string, // What to measure (see available measures above) + "aggregation": string // How to aggregate: "sum", "avg", "count", "max", "min", "p50", "p75", "p90", "p95", "p99", "histogram" + } + ], + "filters": [ // Optional. Default: [] + { + "column": string, // Column to filter on (any dimension field) + "operator": string, // Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject/numberObject: same as string/number with required "key" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Value to compare against + "type": string, // Data type: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "key": string // Required only for stringObject/numberObject types (e.g., metadata filtering) + } + ], + "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time + "granularity": string // One of "auto", "minute", "hour", "day", "week", "month" + }, + "fromTimestamp": string, // Required. ISO datetime string for start of time range + "toTimestamp": string, // Required. ISO datetime string for end of time range (must be after fromTimestamp) + "orderBy": [ // Optional. Default: null + { + "field": string, // Field to order by (dimension or metric alias) + "direction": string // "asc" or "desc" + } + ], + "config": { // Optional. Query-specific configuration + "bins": number, // Optional. Number of bins for histogram aggregation (1-100), default: 10 + "row_limit": number // Optional. Maximum number of rows to return (1-1000), default: 100 + } + } + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MetricsV2Response] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/v2/metrics", + method="GET", + params={ + "query": query, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MetricsV2Response, + parse_obj_as( + type_=MetricsV2Response, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/metrics/types/__init__.py b/langfuse/api/metrics/types/__init__.py new file mode 100644 index 000000000..b9510d24f --- /dev/null +++ b/langfuse/api/metrics/types/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .metrics_v2response import MetricsV2Response +_dynamic_imports: typing.Dict[str, str] = {"MetricsV2Response": ".metrics_v2response"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["MetricsV2Response"] diff --git a/langfuse/api/metrics/types/metrics_v2response.py b/langfuse/api/metrics/types/metrics_v2response.py new file mode 100644 index 000000000..461eaf178 --- /dev/null +++ b/langfuse/api/metrics/types/metrics_v2response.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class MetricsV2Response(UniversalBaseModel): + data: typing.List[typing.Dict[str, typing.Any]] = pydantic.Field() + """ + The metrics data. Each item in the list contains the metric values and dimensions requested in the query. + Format varies based on the query parameters. + Histograms will return an array with [lower, upper, height] tuples. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/models/__init__.py b/langfuse/api/models/__init__.py new file mode 100644 index 000000000..7ebdb7762 --- /dev/null +++ b/langfuse/api/models/__init__.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import CreateModelRequest, PaginatedModels +_dynamic_imports: typing.Dict[str, str] = { + "CreateModelRequest": ".types", + "PaginatedModels": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateModelRequest", "PaginatedModels"] diff --git a/langfuse/api/models/client.py b/langfuse/api/models/client.py new file mode 100644 index 000000000..9f817b8f6 --- /dev/null +++ b/langfuse/api/models/client.py @@ -0,0 +1,523 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..commons.types.model import Model +from ..commons.types.model_usage_unit import ModelUsageUnit +from ..commons.types.pricing_tier_input import PricingTierInput +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawModelsClient, RawModelsClient +from .types.paginated_models import PaginatedModels + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ModelsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawModelsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawModelsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawModelsClient + """ + return self._raw_client + + def create( + self, + *, + model_name: str, + match_pattern: str, + start_date: typing.Optional[dt.datetime] = OMIT, + unit: typing.Optional[ModelUsageUnit] = OMIT, + input_price: typing.Optional[float] = OMIT, + output_price: typing.Optional[float] = OMIT, + total_price: typing.Optional[float] = OMIT, + pricing_tiers: typing.Optional[typing.Sequence[PricingTierInput]] = OMIT, + tokenizer_id: typing.Optional[str] = OMIT, + tokenizer_config: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Model: + """ + Create a model + + Parameters + ---------- + model_name : str + Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime PaginatedModels: + """ + Get all models + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedModels + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.models.list() + """ + _response = self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + def get( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> Model: + """ + Get a model + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Model + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.models.get( + id="id", + ) + """ + _response = self._raw_client.get(id, request_options=request_options) + return _response.data + + def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.models.delete( + id="id", + ) + """ + _response = self._raw_client.delete(id, request_options=request_options) + return _response.data + + +class AsyncModelsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawModelsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawModelsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawModelsClient + """ + return self._raw_client + + async def create( + self, + *, + model_name: str, + match_pattern: str, + start_date: typing.Optional[dt.datetime] = OMIT, + unit: typing.Optional[ModelUsageUnit] = OMIT, + input_price: typing.Optional[float] = OMIT, + output_price: typing.Optional[float] = OMIT, + total_price: typing.Optional[float] = OMIT, + pricing_tiers: typing.Optional[typing.Sequence[PricingTierInput]] = OMIT, + tokenizer_id: typing.Optional[str] = OMIT, + tokenizer_config: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Model: + """ + Create a model + + Parameters + ---------- + model_name : str + Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime None: + await client.models.create( + model_name="modelName", + match_pattern="matchPattern", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + model_name=model_name, + match_pattern=match_pattern, + start_date=start_date, + unit=unit, + input_price=input_price, + output_price=output_price, + total_price=total_price, + pricing_tiers=pricing_tiers, + tokenizer_id=tokenizer_id, + tokenizer_config=tokenizer_config, + request_options=request_options, + ) + return _response.data + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedModels: + """ + Get all models + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedModels + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.models.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def get( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> Model: + """ + Get a model + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Model + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.models.get( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(id, request_options=request_options) + return _response.data + + async def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.models.delete( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(id, request_options=request_options) + return _response.data diff --git a/langfuse/api/models/raw_client.py b/langfuse/api/models/raw_client.py new file mode 100644 index 000000000..0fdc72319 --- /dev/null +++ b/langfuse/api/models/raw_client.py @@ -0,0 +1,993 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.model import Model +from ..commons.types.model_usage_unit import ModelUsageUnit +from ..commons.types.pricing_tier_input import PricingTierInput +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from .types.paginated_models import PaginatedModels + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawModelsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + model_name: str, + match_pattern: str, + start_date: typing.Optional[dt.datetime] = OMIT, + unit: typing.Optional[ModelUsageUnit] = OMIT, + input_price: typing.Optional[float] = OMIT, + output_price: typing.Optional[float] = OMIT, + total_price: typing.Optional[float] = OMIT, + pricing_tiers: typing.Optional[typing.Sequence[PricingTierInput]] = OMIT, + tokenizer_id: typing.Optional[str] = OMIT, + tokenizer_config: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Model]: + """ + Create a model + + Parameters + ---------- + model_name : str + Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime HttpResponse[PaginatedModels]: + """ + Get all models + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedModels] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/models", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedModels, + parse_obj_as( + type_=PaginatedModels, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[Model]: + """ + Get a model + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Model] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/models/{jsonable_encoder(id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Model, + parse_obj_as( + type_=Model, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/models/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawModelsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + model_name: str, + match_pattern: str, + start_date: typing.Optional[dt.datetime] = OMIT, + unit: typing.Optional[ModelUsageUnit] = OMIT, + input_price: typing.Optional[float] = OMIT, + output_price: typing.Optional[float] = OMIT, + total_price: typing.Optional[float] = OMIT, + pricing_tiers: typing.Optional[typing.Sequence[PricingTierInput]] = OMIT, + tokenizer_id: typing.Optional[str] = OMIT, + tokenizer_config: typing.Optional[typing.Any] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Model]: + """ + Create a model + + Parameters + ---------- + model_name : str + Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime AsyncHttpResponse[PaginatedModels]: + """ + Get all models + + Parameters + ---------- + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedModels] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/models", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedModels, + parse_obj_as( + type_=PaginatedModels, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[Model]: + """ + Get a model + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Model] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/models/{jsonable_encoder(id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Model, + parse_obj_as( + type_=Model, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though. + + Parameters + ---------- + id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/models/{jsonable_encoder(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/models/types/__init__.py b/langfuse/api/models/types/__init__.py new file mode 100644 index 000000000..8b4b651c5 --- /dev/null +++ b/langfuse/api/models/types/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_model_request import CreateModelRequest + from .paginated_models import PaginatedModels +_dynamic_imports: typing.Dict[str, str] = { + "CreateModelRequest": ".create_model_request", + "PaginatedModels": ".paginated_models", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateModelRequest", "PaginatedModels"] diff --git a/langfuse/api/models/types/create_model_request.py b/langfuse/api/models/types/create_model_request.py new file mode 100644 index 000000000..dc19db944 --- /dev/null +++ b/langfuse/api/models/types/create_model_request.py @@ -0,0 +1,103 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...commons.types.model_usage_unit import ModelUsageUnit +from ...commons.types.pricing_tier_input import PricingTierInput +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class CreateModelRequest(UniversalBaseModel): + model_name: typing_extensions.Annotated[str, FieldMetadata(alias="modelName")] = ( + pydantic.Field() + ) + """ + Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["ObservationsV2Meta", "ObservationsV2Response"] diff --git a/langfuse/api/observations/client.py b/langfuse/api/observations/client.py new file mode 100644 index 000000000..995dba08b --- /dev/null +++ b/langfuse/api/observations/client.py @@ -0,0 +1,544 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..commons.types.observation_level import ObservationLevel +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawObservationsClient, RawObservationsClient +from .types.observations_v2response import ObservationsV2Response + + +class ObservationsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawObservationsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawObservationsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawObservationsClient + """ + return self._raw_client + + def get_many( + self, + *, + fields: typing.Optional[str] = None, + expand_metadata: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + parse_io_as_json: typing.Optional[bool] = None, + name: typing.Optional[str] = None, + user_id: typing.Optional[str] = None, + type: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + level: typing.Optional[ObservationLevel] = None, + parent_observation_id: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + from_start_time: typing.Optional[dt.datetime] = None, + to_start_time: typing.Optional[dt.datetime] = None, + version: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ObservationsV2Response: + """ + Get a list of observations with cursor-based pagination and flexible field selection. + + ## Cursor-based Pagination + This endpoint uses cursor-based pagination for efficient traversal of large datasets. + The cursor is returned in the response metadata and should be passed in subsequent requests + to retrieve the next page of results. + + ## Field Selection + Use the `fields` parameter to control which observation fields are returned: + - `core` - Always included: id, traceId, startTime, endTime, projectId, parentObservationId, type + - `basic` - name, level, statusMessage, version, environment, bookmarked, public, userId, sessionId + - `time` - completionStartTime, createdAt, updatedAt + - `io` - input, output + - `metadata` - metadata (truncated to 200 chars by default, use `expandMetadata` to get full values) + - `model` - providedModelName, internalModelId, modelParameters + - `usage` - usageDetails, costDetails, totalCost, usagePricingTierName + - `prompt` - promptId, promptName, promptVersion + - `metrics` - latency, timeToFirstToken + - `trace_context` - tags, release, traceName + + If not specified, `core` and `basic` field groups are returned. + + ## Filters + Multiple filtering options are available via query parameters or the structured `filter` parameter. + When using the `filter` parameter, it takes precedence over individual query parameter filters. + + Parameters + ---------- + fields : typing.Optional[str] + Comma-separated list of field groups to include in the response. + Available groups: core, basic, time, io, metadata, model, usage, prompt, metrics, trace_context. + If not specified, `core` and `basic` field groups are returned. + Example: "basic,usage,model" + + expand_metadata : typing.Optional[str] + Comma-separated list of metadata keys to return non-truncated. + By default, metadata values over 200 characters are truncated. + Use this parameter to retrieve full values for specific keys. + Example: "key1,key2" + + limit : typing.Optional[int] + Number of items to return per page. Maximum 1000, default 50. + + cursor : typing.Optional[str] + Base64-encoded cursor for pagination. Use the cursor from the previous response to get the next page. + + parse_io_as_json : typing.Optional[bool] + **Deprecated.** Setting this to `true` will return a 400 error. + Input/output fields are always returned as raw strings. + Remove this parameter or set it to `false`. + + name : typing.Optional[str] + + user_id : typing.Optional[str] + + type : typing.Optional[str] + Filter by observation type (e.g., "GENERATION", "SPAN", "EVENT", "AGENT", "TOOL", "CHAIN", "RETRIEVER", "EVALUATOR", "EMBEDDING", "GUARDRAIL") + + trace_id : typing.Optional[str] + + level : typing.Optional[ObservationLevel] + Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). + + parent_observation_id : typing.Optional[str] + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for observations where the environment is one of the provided values. + + from_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time on or after this datetime (ISO 8601). + + to_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time before this datetime (ISO 8601). + + version : typing.Optional[str] + Optional filter to only include observations with a certain version. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with", "matches" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "matches" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Observation Fields + - `id` (string) - Observation ID + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + - `name` (string) - Observation name + - `traceId` (string) - Associated trace ID + - `startTime` (datetime) - Observation start time + - `endTime` (datetime) - Observation end time + - `environment` (string) - Environment tag + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + - `statusMessage` (string) - Status message + - `version` (string) - Version tag + - `userId` (string) - User ID + - `sessionId` (string) - Session ID + + ### Trace-Related Fields + - `traceName` (string) - Name of the parent trace + - `traceTags` (arrayOptions) - Tags from the parent trace + - `tags` (arrayOptions) - Alias for traceTags + + ### Performance Metrics + - `latency` (number) - Latency in seconds (calculated: end_time - start_time) + - `timeToFirstToken` (number) - Time to first token in seconds + - `tokensPerSecond` (number) - Output tokens per second + + ### Token Usage + - `inputTokens` (number) - Number of input tokens + - `outputTokens` (number) - Number of output tokens + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + ### Cost Metrics + - `inputCost` (number) - Input cost in USD + - `outputCost` (number) - Output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Model Information + - `model` (string) - Provided model name (alias: `providedModelName`) + - `promptName` (string) - Associated prompt name + - `promptVersion` (number) - Associated prompt version + + ### Structured Data + - `input` (string) - Observation input. Supports accelerated indexed literal search with the `matches` operator. + - `output` (string) - Observation output. Supports accelerated indexed literal search with the `matches` operator. + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + The `matches` operator is only supported for `input`, `output`, and stringObject `metadata` filters. It performs indexed literal search with token-boundary pruning using the events table text indexes. Case sensitivity differs by target: `input` and `output` matches are case-insensitive, while metadata value matches are case-sensitive. Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. Use `contains` for legacy substring semantics where the API allows it. Any v2 `input` or `output` filter must be accompanied by at least one `=` or `matches` filter on `input` or `output`; standalone `contains`, `starts with`, `ends with`, and `does not contain` filters on these columns are rejected. + + ## Filter Examples + ```json + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + }, + { + "type": "string", + "column": "output", + "operator": "matches", + "value": "needle" + } + ] + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ObservationsV2Response + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.observations.get_many() + """ + _response = self._raw_client.get_many( + fields=fields, + expand_metadata=expand_metadata, + limit=limit, + cursor=cursor, + parse_io_as_json=parse_io_as_json, + name=name, + user_id=user_id, + type=type, + trace_id=trace_id, + level=level, + parent_observation_id=parent_observation_id, + environment=environment, + from_start_time=from_start_time, + to_start_time=to_start_time, + version=version, + filter=filter, + request_options=request_options, + ) + return _response.data + + +class AsyncObservationsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawObservationsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawObservationsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawObservationsClient + """ + return self._raw_client + + async def get_many( + self, + *, + fields: typing.Optional[str] = None, + expand_metadata: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + parse_io_as_json: typing.Optional[bool] = None, + name: typing.Optional[str] = None, + user_id: typing.Optional[str] = None, + type: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + level: typing.Optional[ObservationLevel] = None, + parent_observation_id: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + from_start_time: typing.Optional[dt.datetime] = None, + to_start_time: typing.Optional[dt.datetime] = None, + version: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ObservationsV2Response: + """ + Get a list of observations with cursor-based pagination and flexible field selection. + + ## Cursor-based Pagination + This endpoint uses cursor-based pagination for efficient traversal of large datasets. + The cursor is returned in the response metadata and should be passed in subsequent requests + to retrieve the next page of results. + + ## Field Selection + Use the `fields` parameter to control which observation fields are returned: + - `core` - Always included: id, traceId, startTime, endTime, projectId, parentObservationId, type + - `basic` - name, level, statusMessage, version, environment, bookmarked, public, userId, sessionId + - `time` - completionStartTime, createdAt, updatedAt + - `io` - input, output + - `metadata` - metadata (truncated to 200 chars by default, use `expandMetadata` to get full values) + - `model` - providedModelName, internalModelId, modelParameters + - `usage` - usageDetails, costDetails, totalCost, usagePricingTierName + - `prompt` - promptId, promptName, promptVersion + - `metrics` - latency, timeToFirstToken + - `trace_context` - tags, release, traceName + + If not specified, `core` and `basic` field groups are returned. + + ## Filters + Multiple filtering options are available via query parameters or the structured `filter` parameter. + When using the `filter` parameter, it takes precedence over individual query parameter filters. + + Parameters + ---------- + fields : typing.Optional[str] + Comma-separated list of field groups to include in the response. + Available groups: core, basic, time, io, metadata, model, usage, prompt, metrics, trace_context. + If not specified, `core` and `basic` field groups are returned. + Example: "basic,usage,model" + + expand_metadata : typing.Optional[str] + Comma-separated list of metadata keys to return non-truncated. + By default, metadata values over 200 characters are truncated. + Use this parameter to retrieve full values for specific keys. + Example: "key1,key2" + + limit : typing.Optional[int] + Number of items to return per page. Maximum 1000, default 50. + + cursor : typing.Optional[str] + Base64-encoded cursor for pagination. Use the cursor from the previous response to get the next page. + + parse_io_as_json : typing.Optional[bool] + **Deprecated.** Setting this to `true` will return a 400 error. + Input/output fields are always returned as raw strings. + Remove this parameter or set it to `false`. + + name : typing.Optional[str] + + user_id : typing.Optional[str] + + type : typing.Optional[str] + Filter by observation type (e.g., "GENERATION", "SPAN", "EVENT", "AGENT", "TOOL", "CHAIN", "RETRIEVER", "EVALUATOR", "EMBEDDING", "GUARDRAIL") + + trace_id : typing.Optional[str] + + level : typing.Optional[ObservationLevel] + Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). + + parent_observation_id : typing.Optional[str] + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for observations where the environment is one of the provided values. + + from_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time on or after this datetime (ISO 8601). + + to_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time before this datetime (ISO 8601). + + version : typing.Optional[str] + Optional filter to only include observations with a certain version. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with", "matches" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "matches" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Observation Fields + - `id` (string) - Observation ID + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + - `name` (string) - Observation name + - `traceId` (string) - Associated trace ID + - `startTime` (datetime) - Observation start time + - `endTime` (datetime) - Observation end time + - `environment` (string) - Environment tag + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + - `statusMessage` (string) - Status message + - `version` (string) - Version tag + - `userId` (string) - User ID + - `sessionId` (string) - Session ID + + ### Trace-Related Fields + - `traceName` (string) - Name of the parent trace + - `traceTags` (arrayOptions) - Tags from the parent trace + - `tags` (arrayOptions) - Alias for traceTags + + ### Performance Metrics + - `latency` (number) - Latency in seconds (calculated: end_time - start_time) + - `timeToFirstToken` (number) - Time to first token in seconds + - `tokensPerSecond` (number) - Output tokens per second + + ### Token Usage + - `inputTokens` (number) - Number of input tokens + - `outputTokens` (number) - Number of output tokens + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + ### Cost Metrics + - `inputCost` (number) - Input cost in USD + - `outputCost` (number) - Output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Model Information + - `model` (string) - Provided model name (alias: `providedModelName`) + - `promptName` (string) - Associated prompt name + - `promptVersion` (number) - Associated prompt version + + ### Structured Data + - `input` (string) - Observation input. Supports accelerated indexed literal search with the `matches` operator. + - `output` (string) - Observation output. Supports accelerated indexed literal search with the `matches` operator. + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + The `matches` operator is only supported for `input`, `output`, and stringObject `metadata` filters. It performs indexed literal search with token-boundary pruning using the events table text indexes. Case sensitivity differs by target: `input` and `output` matches are case-insensitive, while metadata value matches are case-sensitive. Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. Use `contains` for legacy substring semantics where the API allows it. Any v2 `input` or `output` filter must be accompanied by at least one `=` or `matches` filter on `input` or `output`; standalone `contains`, `starts with`, `ends with`, and `does not contain` filters on these columns are rejected. + + ## Filter Examples + ```json + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + }, + { + "type": "string", + "column": "output", + "operator": "matches", + "value": "needle" + } + ] + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ObservationsV2Response + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.observations.get_many() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_many( + fields=fields, + expand_metadata=expand_metadata, + limit=limit, + cursor=cursor, + parse_io_as_json=parse_io_as_json, + name=name, + user_id=user_id, + type=type, + trace_id=trace_id, + level=level, + parent_observation_id=parent_observation_id, + environment=environment, + from_start_time=from_start_time, + to_start_time=to_start_time, + version=version, + filter=filter, + request_options=request_options, + ) + return _response.data diff --git a/langfuse/api/observations/raw_client.py b/langfuse/api/observations/raw_client.py new file mode 100644 index 000000000..017199252 --- /dev/null +++ b/langfuse/api/observations/raw_client.py @@ -0,0 +1,663 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.observation_level import ObservationLevel +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.observations_v2response import ObservationsV2Response + + +class RawObservationsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_many( + self, + *, + fields: typing.Optional[str] = None, + expand_metadata: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + parse_io_as_json: typing.Optional[bool] = None, + name: typing.Optional[str] = None, + user_id: typing.Optional[str] = None, + type: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + level: typing.Optional[ObservationLevel] = None, + parent_observation_id: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + from_start_time: typing.Optional[dt.datetime] = None, + to_start_time: typing.Optional[dt.datetime] = None, + version: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ObservationsV2Response]: + """ + Get a list of observations with cursor-based pagination and flexible field selection. + + ## Cursor-based Pagination + This endpoint uses cursor-based pagination for efficient traversal of large datasets. + The cursor is returned in the response metadata and should be passed in subsequent requests + to retrieve the next page of results. + + ## Field Selection + Use the `fields` parameter to control which observation fields are returned: + - `core` - Always included: id, traceId, startTime, endTime, projectId, parentObservationId, type + - `basic` - name, level, statusMessage, version, environment, bookmarked, public, userId, sessionId + - `time` - completionStartTime, createdAt, updatedAt + - `io` - input, output + - `metadata` - metadata (truncated to 200 chars by default, use `expandMetadata` to get full values) + - `model` - providedModelName, internalModelId, modelParameters + - `usage` - usageDetails, costDetails, totalCost, usagePricingTierName + - `prompt` - promptId, promptName, promptVersion + - `metrics` - latency, timeToFirstToken + - `trace_context` - tags, release, traceName + + If not specified, `core` and `basic` field groups are returned. + + ## Filters + Multiple filtering options are available via query parameters or the structured `filter` parameter. + When using the `filter` parameter, it takes precedence over individual query parameter filters. + + Parameters + ---------- + fields : typing.Optional[str] + Comma-separated list of field groups to include in the response. + Available groups: core, basic, time, io, metadata, model, usage, prompt, metrics, trace_context. + If not specified, `core` and `basic` field groups are returned. + Example: "basic,usage,model" + + expand_metadata : typing.Optional[str] + Comma-separated list of metadata keys to return non-truncated. + By default, metadata values over 200 characters are truncated. + Use this parameter to retrieve full values for specific keys. + Example: "key1,key2" + + limit : typing.Optional[int] + Number of items to return per page. Maximum 1000, default 50. + + cursor : typing.Optional[str] + Base64-encoded cursor for pagination. Use the cursor from the previous response to get the next page. + + parse_io_as_json : typing.Optional[bool] + **Deprecated.** Setting this to `true` will return a 400 error. + Input/output fields are always returned as raw strings. + Remove this parameter or set it to `false`. + + name : typing.Optional[str] + + user_id : typing.Optional[str] + + type : typing.Optional[str] + Filter by observation type (e.g., "GENERATION", "SPAN", "EVENT", "AGENT", "TOOL", "CHAIN", "RETRIEVER", "EVALUATOR", "EMBEDDING", "GUARDRAIL") + + trace_id : typing.Optional[str] + + level : typing.Optional[ObservationLevel] + Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). + + parent_observation_id : typing.Optional[str] + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for observations where the environment is one of the provided values. + + from_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time on or after this datetime (ISO 8601). + + to_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time before this datetime (ISO 8601). + + version : typing.Optional[str] + Optional filter to only include observations with a certain version. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with", "matches" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "matches" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Observation Fields + - `id` (string) - Observation ID + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + - `name` (string) - Observation name + - `traceId` (string) - Associated trace ID + - `startTime` (datetime) - Observation start time + - `endTime` (datetime) - Observation end time + - `environment` (string) - Environment tag + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + - `statusMessage` (string) - Status message + - `version` (string) - Version tag + - `userId` (string) - User ID + - `sessionId` (string) - Session ID + + ### Trace-Related Fields + - `traceName` (string) - Name of the parent trace + - `traceTags` (arrayOptions) - Tags from the parent trace + - `tags` (arrayOptions) - Alias for traceTags + + ### Performance Metrics + - `latency` (number) - Latency in seconds (calculated: end_time - start_time) + - `timeToFirstToken` (number) - Time to first token in seconds + - `tokensPerSecond` (number) - Output tokens per second + + ### Token Usage + - `inputTokens` (number) - Number of input tokens + - `outputTokens` (number) - Number of output tokens + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + ### Cost Metrics + - `inputCost` (number) - Input cost in USD + - `outputCost` (number) - Output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Model Information + - `model` (string) - Provided model name (alias: `providedModelName`) + - `promptName` (string) - Associated prompt name + - `promptVersion` (number) - Associated prompt version + + ### Structured Data + - `input` (string) - Observation input. Supports accelerated indexed literal search with the `matches` operator. + - `output` (string) - Observation output. Supports accelerated indexed literal search with the `matches` operator. + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + The `matches` operator is only supported for `input`, `output`, and stringObject `metadata` filters. It performs indexed literal search with token-boundary pruning using the events table text indexes. Case sensitivity differs by target: `input` and `output` matches are case-insensitive, while metadata value matches are case-sensitive. Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. Use `contains` for legacy substring semantics where the API allows it. Any v2 `input` or `output` filter must be accompanied by at least one `=` or `matches` filter on `input` or `output`; standalone `contains`, `starts with`, `ends with`, and `does not contain` filters on these columns are rejected. + + ## Filter Examples + ```json + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + }, + { + "type": "string", + "column": "output", + "operator": "matches", + "value": "needle" + } + ] + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ObservationsV2Response] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/v2/observations", + method="GET", + params={ + "fields": fields, + "expandMetadata": expand_metadata, + "limit": limit, + "cursor": cursor, + "parseIoAsJson": parse_io_as_json, + "name": name, + "userId": user_id, + "type": type, + "traceId": trace_id, + "level": level, + "parentObservationId": parent_observation_id, + "environment": environment, + "fromStartTime": serialize_datetime(from_start_time) + if from_start_time is not None + else None, + "toStartTime": serialize_datetime(to_start_time) + if to_start_time is not None + else None, + "version": version, + "filter": filter, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ObservationsV2Response, + parse_obj_as( + type_=ObservationsV2Response, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawObservationsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_many( + self, + *, + fields: typing.Optional[str] = None, + expand_metadata: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + parse_io_as_json: typing.Optional[bool] = None, + name: typing.Optional[str] = None, + user_id: typing.Optional[str] = None, + type: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + level: typing.Optional[ObservationLevel] = None, + parent_observation_id: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + from_start_time: typing.Optional[dt.datetime] = None, + to_start_time: typing.Optional[dt.datetime] = None, + version: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ObservationsV2Response]: + """ + Get a list of observations with cursor-based pagination and flexible field selection. + + ## Cursor-based Pagination + This endpoint uses cursor-based pagination for efficient traversal of large datasets. + The cursor is returned in the response metadata and should be passed in subsequent requests + to retrieve the next page of results. + + ## Field Selection + Use the `fields` parameter to control which observation fields are returned: + - `core` - Always included: id, traceId, startTime, endTime, projectId, parentObservationId, type + - `basic` - name, level, statusMessage, version, environment, bookmarked, public, userId, sessionId + - `time` - completionStartTime, createdAt, updatedAt + - `io` - input, output + - `metadata` - metadata (truncated to 200 chars by default, use `expandMetadata` to get full values) + - `model` - providedModelName, internalModelId, modelParameters + - `usage` - usageDetails, costDetails, totalCost, usagePricingTierName + - `prompt` - promptId, promptName, promptVersion + - `metrics` - latency, timeToFirstToken + - `trace_context` - tags, release, traceName + + If not specified, `core` and `basic` field groups are returned. + + ## Filters + Multiple filtering options are available via query parameters or the structured `filter` parameter. + When using the `filter` parameter, it takes precedence over individual query parameter filters. + + Parameters + ---------- + fields : typing.Optional[str] + Comma-separated list of field groups to include in the response. + Available groups: core, basic, time, io, metadata, model, usage, prompt, metrics, trace_context. + If not specified, `core` and `basic` field groups are returned. + Example: "basic,usage,model" + + expand_metadata : typing.Optional[str] + Comma-separated list of metadata keys to return non-truncated. + By default, metadata values over 200 characters are truncated. + Use this parameter to retrieve full values for specific keys. + Example: "key1,key2" + + limit : typing.Optional[int] + Number of items to return per page. Maximum 1000, default 50. + + cursor : typing.Optional[str] + Base64-encoded cursor for pagination. Use the cursor from the previous response to get the next page. + + parse_io_as_json : typing.Optional[bool] + **Deprecated.** Setting this to `true` will return a 400 error. + Input/output fields are always returned as raw strings. + Remove this parameter or set it to `false`. + + name : typing.Optional[str] + + user_id : typing.Optional[str] + + type : typing.Optional[str] + Filter by observation type (e.g., "GENERATION", "SPAN", "EVENT", "AGENT", "TOOL", "CHAIN", "RETRIEVER", "EVALUATOR", "EMBEDDING", "GUARDRAIL") + + trace_id : typing.Optional[str] + + level : typing.Optional[ObservationLevel] + Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). + + parent_observation_id : typing.Optional[str] + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for observations where the environment is one of the provided values. + + from_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time on or after this datetime (ISO 8601). + + to_start_time : typing.Optional[dt.datetime] + Retrieve only observations with a start_time before this datetime (ISO 8601). + + version : typing.Optional[str] + Optional filter to only include observations with a certain version. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with", "matches" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "matches" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Observation Fields + - `id` (string) - Observation ID + - `type` (string) - Observation type (SPAN, GENERATION, EVENT) + - `name` (string) - Observation name + - `traceId` (string) - Associated trace ID + - `startTime` (datetime) - Observation start time + - `endTime` (datetime) - Observation end time + - `environment` (string) - Environment tag + - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) + - `statusMessage` (string) - Status message + - `version` (string) - Version tag + - `userId` (string) - User ID + - `sessionId` (string) - Session ID + + ### Trace-Related Fields + - `traceName` (string) - Name of the parent trace + - `traceTags` (arrayOptions) - Tags from the parent trace + - `tags` (arrayOptions) - Alias for traceTags + + ### Performance Metrics + - `latency` (number) - Latency in seconds (calculated: end_time - start_time) + - `timeToFirstToken` (number) - Time to first token in seconds + - `tokensPerSecond` (number) - Output tokens per second + + ### Token Usage + - `inputTokens` (number) - Number of input tokens + - `outputTokens` (number) - Number of output tokens + - `totalTokens` (number) - Total tokens (alias: `tokens`) + + ### Cost Metrics + - `inputCost` (number) - Input cost in USD + - `outputCost` (number) - Output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Model Information + - `model` (string) - Provided model name (alias: `providedModelName`) + - `promptName` (string) - Associated prompt name + - `promptVersion` (number) - Associated prompt version + + ### Structured Data + - `input` (string) - Observation input. Supports accelerated indexed literal search with the `matches` operator. + - `output` (string) - Observation output. Supports accelerated indexed literal search with the `matches` operator. + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + The `matches` operator is only supported for `input`, `output`, and stringObject `metadata` filters. It performs indexed literal search with token-boundary pruning using the events table text indexes. Case sensitivity differs by target: `input` and `output` matches are case-insensitive, while metadata value matches are case-sensitive. Unlike SQL `LIKE`, `%` and `_` are treated as literal characters. Use `contains` for legacy substring semantics where the API allows it. Any v2 `input` or `output` filter must be accompanied by at least one `=` or `matches` filter on `input` or `output`; standalone `contains`, `starts with`, `ends with`, and `does not contain` filters on these columns are rejected. + + ## Filter Examples + ```json + [ + { + "type": "string", + "column": "type", + "operator": "=", + "value": "GENERATION" + }, + { + "type": "number", + "column": "latency", + "operator": ">=", + "value": 2.5 + }, + { + "type": "stringObject", + "column": "metadata", + "key": "environment", + "operator": "=", + "value": "production" + }, + { + "type": "string", + "column": "output", + "operator": "matches", + "value": "needle" + } + ] + ``` + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ObservationsV2Response] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/v2/observations", + method="GET", + params={ + "fields": fields, + "expandMetadata": expand_metadata, + "limit": limit, + "cursor": cursor, + "parseIoAsJson": parse_io_as_json, + "name": name, + "userId": user_id, + "type": type, + "traceId": trace_id, + "level": level, + "parentObservationId": parent_observation_id, + "environment": environment, + "fromStartTime": serialize_datetime(from_start_time) + if from_start_time is not None + else None, + "toStartTime": serialize_datetime(to_start_time) + if to_start_time is not None + else None, + "version": version, + "filter": filter, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ObservationsV2Response, + parse_obj_as( + type_=ObservationsV2Response, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/observations/types/__init__.py b/langfuse/api/observations/types/__init__.py new file mode 100644 index 000000000..6e132aba6 --- /dev/null +++ b/langfuse/api/observations/types/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .observations_v2meta import ObservationsV2Meta + from .observations_v2response import ObservationsV2Response +_dynamic_imports: typing.Dict[str, str] = { + "ObservationsV2Meta": ".observations_v2meta", + "ObservationsV2Response": ".observations_v2response", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["ObservationsV2Meta", "ObservationsV2Response"] diff --git a/langfuse/api/observations/types/observations_v2meta.py b/langfuse/api/observations/types/observations_v2meta.py new file mode 100644 index 000000000..8f86a6512 --- /dev/null +++ b/langfuse/api/observations/types/observations_v2meta.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ObservationsV2Meta(UniversalBaseModel): + """ + Metadata for cursor-based pagination + """ + + cursor: typing.Optional[str] = pydantic.Field(default=None) + """ + Base64-encoded cursor to use for retrieving the next page. If not present, there are no more results. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/observations/types/observations_v2response.py b/langfuse/api/observations/types/observations_v2response.py new file mode 100644 index 000000000..0ee1fb8bd --- /dev/null +++ b/langfuse/api/observations/types/observations_v2response.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.observation_v2 import ObservationV2 +from ...core.pydantic_utilities import UniversalBaseModel +from .observations_v2meta import ObservationsV2Meta + + +class ObservationsV2Response(UniversalBaseModel): + """ + Response containing observations with field-group-based filtering and cursor-based pagination. + + The `data` array contains observation objects with only the requested field groups included. + Use the `cursor` in `meta` to retrieve the next page of results. + """ + + data: typing.List[ObservationV2] = pydantic.Field() + """ + Array of observation objects. Fields included depend on the `fields` parameter in the request. + """ + + meta: ObservationsV2Meta + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/opentelemetry/__init__.py b/langfuse/api/opentelemetry/__init__.py new file mode 100644 index 000000000..30caa3796 --- /dev/null +++ b/langfuse/api/opentelemetry/__init__.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + OtelAttribute, + OtelAttributeValue, + OtelResource, + OtelResourceSpan, + OtelScope, + OtelScopeSpan, + OtelSpan, + OtelTraceResponse, + ) +_dynamic_imports: typing.Dict[str, str] = { + "OtelAttribute": ".types", + "OtelAttributeValue": ".types", + "OtelResource": ".types", + "OtelResourceSpan": ".types", + "OtelScope": ".types", + "OtelScopeSpan": ".types", + "OtelSpan": ".types", + "OtelTraceResponse": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "OtelAttribute", + "OtelAttributeValue", + "OtelResource", + "OtelResourceSpan", + "OtelScope", + "OtelScopeSpan", + "OtelSpan", + "OtelTraceResponse", +] diff --git a/langfuse/api/opentelemetry/client.py b/langfuse/api/opentelemetry/client.py new file mode 100644 index 000000000..13177e5e6 --- /dev/null +++ b/langfuse/api/opentelemetry/client.py @@ -0,0 +1,276 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawOpentelemetryClient, RawOpentelemetryClient +from .types.otel_resource_span import OtelResourceSpan +from .types.otel_trace_response import OtelTraceResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class OpentelemetryClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawOpentelemetryClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawOpentelemetryClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawOpentelemetryClient + """ + return self._raw_client + + def export_traces( + self, + *, + resource_spans: typing.Sequence[OtelResourceSpan], + request_options: typing.Optional[RequestOptions] = None, + ) -> OtelTraceResponse: + """ + **OpenTelemetry Traces Ingestion Endpoint** + + This endpoint implements the OTLP/HTTP specification for trace ingestion, providing native OpenTelemetry integration for Langfuse Observability. + + **Supported Formats:** + - Binary Protobuf: `Content-Type: application/x-protobuf` + - JSON Protobuf: `Content-Type: application/json` + - Supports gzip compression via `Content-Encoding: gzip` header + + **Specification Compliance:** + - Conforms to [OTLP/HTTP Trace Export](https://opentelemetry.io/docs/specs/otlp/#otlphttp) + - Implements `ExportTraceServiceRequest` message format + + **Documentation:** + - Integration guide: https://langfuse.com/integrations/native/opentelemetry + - Data model: https://langfuse.com/docs/observability/data-model + + Parameters + ---------- + resource_spans : typing.Sequence[OtelResourceSpan] + Array of resource spans containing trace data as defined in the OTLP specification + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + OtelTraceResponse + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.opentelemetry import ( + OtelAttribute, + OtelAttributeValue, + OtelResource, + OtelResourceSpan, + OtelScope, + OtelScopeSpan, + OtelSpan, + ) + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.opentelemetry.export_traces( + resource_spans=[ + OtelResourceSpan( + resource=OtelResource( + attributes=[ + OtelAttribute( + key="service.name", + value=OtelAttributeValue( + string_value="my-service", + ), + ), + OtelAttribute( + key="service.version", + value=OtelAttributeValue( + string_value="1.0.0", + ), + ), + ], + ), + scope_spans=[ + OtelScopeSpan( + scope=OtelScope( + name="langfuse-sdk", + version="2.60.3", + ), + spans=[ + OtelSpan( + trace_id="0123456789abcdef0123456789abcdef", + span_id="0123456789abcdef", + name="my-operation", + kind=1, + start_time_unix_nano="1747872000000000000", + end_time_unix_nano="1747872001000000000", + attributes=[ + OtelAttribute( + key="langfuse.observation.type", + value=OtelAttributeValue( + string_value="generation", + ), + ) + ], + status={}, + ) + ], + ) + ], + ) + ], + ) + """ + _response = self._raw_client.export_traces( + resource_spans=resource_spans, request_options=request_options + ) + return _response.data + + +class AsyncOpentelemetryClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawOpentelemetryClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawOpentelemetryClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawOpentelemetryClient + """ + return self._raw_client + + async def export_traces( + self, + *, + resource_spans: typing.Sequence[OtelResourceSpan], + request_options: typing.Optional[RequestOptions] = None, + ) -> OtelTraceResponse: + """ + **OpenTelemetry Traces Ingestion Endpoint** + + This endpoint implements the OTLP/HTTP specification for trace ingestion, providing native OpenTelemetry integration for Langfuse Observability. + + **Supported Formats:** + - Binary Protobuf: `Content-Type: application/x-protobuf` + - JSON Protobuf: `Content-Type: application/json` + - Supports gzip compression via `Content-Encoding: gzip` header + + **Specification Compliance:** + - Conforms to [OTLP/HTTP Trace Export](https://opentelemetry.io/docs/specs/otlp/#otlphttp) + - Implements `ExportTraceServiceRequest` message format + + **Documentation:** + - Integration guide: https://langfuse.com/integrations/native/opentelemetry + - Data model: https://langfuse.com/docs/observability/data-model + + Parameters + ---------- + resource_spans : typing.Sequence[OtelResourceSpan] + Array of resource spans containing trace data as defined in the OTLP specification + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + OtelTraceResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.opentelemetry import ( + OtelAttribute, + OtelAttributeValue, + OtelResource, + OtelResourceSpan, + OtelScope, + OtelScopeSpan, + OtelSpan, + ) + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.opentelemetry.export_traces( + resource_spans=[ + OtelResourceSpan( + resource=OtelResource( + attributes=[ + OtelAttribute( + key="service.name", + value=OtelAttributeValue( + string_value="my-service", + ), + ), + OtelAttribute( + key="service.version", + value=OtelAttributeValue( + string_value="1.0.0", + ), + ), + ], + ), + scope_spans=[ + OtelScopeSpan( + scope=OtelScope( + name="langfuse-sdk", + version="2.60.3", + ), + spans=[ + OtelSpan( + trace_id="0123456789abcdef0123456789abcdef", + span_id="0123456789abcdef", + name="my-operation", + kind=1, + start_time_unix_nano="1747872000000000000", + end_time_unix_nano="1747872001000000000", + attributes=[ + OtelAttribute( + key="langfuse.observation.type", + value=OtelAttributeValue( + string_value="generation", + ), + ) + ], + status={}, + ) + ], + ) + ], + ) + ], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.export_traces( + resource_spans=resource_spans, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/opentelemetry/raw_client.py b/langfuse/api/opentelemetry/raw_client.py new file mode 100644 index 000000000..6b68f909b --- /dev/null +++ b/langfuse/api/opentelemetry/raw_client.py @@ -0,0 +1,291 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from .types.otel_resource_span import OtelResourceSpan +from .types.otel_trace_response import OtelTraceResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawOpentelemetryClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def export_traces( + self, + *, + resource_spans: typing.Sequence[OtelResourceSpan], + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[OtelTraceResponse]: + """ + **OpenTelemetry Traces Ingestion Endpoint** + + This endpoint implements the OTLP/HTTP specification for trace ingestion, providing native OpenTelemetry integration for Langfuse Observability. + + **Supported Formats:** + - Binary Protobuf: `Content-Type: application/x-protobuf` + - JSON Protobuf: `Content-Type: application/json` + - Supports gzip compression via `Content-Encoding: gzip` header + + **Specification Compliance:** + - Conforms to [OTLP/HTTP Trace Export](https://opentelemetry.io/docs/specs/otlp/#otlphttp) + - Implements `ExportTraceServiceRequest` message format + + **Documentation:** + - Integration guide: https://langfuse.com/integrations/native/opentelemetry + - Data model: https://langfuse.com/docs/observability/data-model + + Parameters + ---------- + resource_spans : typing.Sequence[OtelResourceSpan] + Array of resource spans containing trace data as defined in the OTLP specification + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[OtelTraceResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/otel/v1/traces", + method="POST", + json={ + "resourceSpans": convert_and_respect_annotation_metadata( + object_=resource_spans, + annotation=typing.Sequence[OtelResourceSpan], + direction="write", + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + OtelTraceResponse, + parse_obj_as( + type_=OtelTraceResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawOpentelemetryClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def export_traces( + self, + *, + resource_spans: typing.Sequence[OtelResourceSpan], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[OtelTraceResponse]: + """ + **OpenTelemetry Traces Ingestion Endpoint** + + This endpoint implements the OTLP/HTTP specification for trace ingestion, providing native OpenTelemetry integration for Langfuse Observability. + + **Supported Formats:** + - Binary Protobuf: `Content-Type: application/x-protobuf` + - JSON Protobuf: `Content-Type: application/json` + - Supports gzip compression via `Content-Encoding: gzip` header + + **Specification Compliance:** + - Conforms to [OTLP/HTTP Trace Export](https://opentelemetry.io/docs/specs/otlp/#otlphttp) + - Implements `ExportTraceServiceRequest` message format + + **Documentation:** + - Integration guide: https://langfuse.com/integrations/native/opentelemetry + - Data model: https://langfuse.com/docs/observability/data-model + + Parameters + ---------- + resource_spans : typing.Sequence[OtelResourceSpan] + Array of resource spans containing trace data as defined in the OTLP specification + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[OtelTraceResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/otel/v1/traces", + method="POST", + json={ + "resourceSpans": convert_and_respect_annotation_metadata( + object_=resource_spans, + annotation=typing.Sequence[OtelResourceSpan], + direction="write", + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + OtelTraceResponse, + parse_obj_as( + type_=OtelTraceResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/opentelemetry/types/__init__.py b/langfuse/api/opentelemetry/types/__init__.py new file mode 100644 index 000000000..ad2fd4899 --- /dev/null +++ b/langfuse/api/opentelemetry/types/__init__.py @@ -0,0 +1,65 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .otel_attribute import OtelAttribute + from .otel_attribute_value import OtelAttributeValue + from .otel_resource import OtelResource + from .otel_resource_span import OtelResourceSpan + from .otel_scope import OtelScope + from .otel_scope_span import OtelScopeSpan + from .otel_span import OtelSpan + from .otel_trace_response import OtelTraceResponse +_dynamic_imports: typing.Dict[str, str] = { + "OtelAttribute": ".otel_attribute", + "OtelAttributeValue": ".otel_attribute_value", + "OtelResource": ".otel_resource", + "OtelResourceSpan": ".otel_resource_span", + "OtelScope": ".otel_scope", + "OtelScopeSpan": ".otel_scope_span", + "OtelSpan": ".otel_span", + "OtelTraceResponse": ".otel_trace_response", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "OtelAttribute", + "OtelAttributeValue", + "OtelResource", + "OtelResourceSpan", + "OtelScope", + "OtelScopeSpan", + "OtelSpan", + "OtelTraceResponse", +] diff --git a/langfuse/api/opentelemetry/types/otel_attribute.py b/langfuse/api/opentelemetry/types/otel_attribute.py new file mode 100644 index 000000000..479a46551 --- /dev/null +++ b/langfuse/api/opentelemetry/types/otel_attribute.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .otel_attribute_value import OtelAttributeValue + + +class OtelAttribute(UniversalBaseModel): + """ + Key-value attribute pair for resources, scopes, or spans + """ + + key: typing.Optional[str] = pydantic.Field(default=None) + """ + Attribute key (e.g., "service.name", "langfuse.observation.type") + """ + + value: typing.Optional[OtelAttributeValue] = pydantic.Field(default=None) + """ + Attribute value + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/opentelemetry/types/otel_attribute_value.py b/langfuse/api/opentelemetry/types/otel_attribute_value.py new file mode 100644 index 000000000..c381dd28f --- /dev/null +++ b/langfuse/api/opentelemetry/types/otel_attribute_value.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class OtelAttributeValue(UniversalBaseModel): + """ + Attribute value wrapper supporting different value types + """ + + string_value: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="stringValue") + ] = pydantic.Field(default=None) + """ + String value + """ + + int_value: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="intValue") + ] = pydantic.Field(default=None) + """ + Integer value + """ + + double_value: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="doubleValue") + ] = pydantic.Field(default=None) + """ + Double value + """ + + bool_value: typing_extensions.Annotated[ + typing.Optional[bool], FieldMetadata(alias="boolValue") + ] = pydantic.Field(default=None) + """ + Boolean value + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/opentelemetry/types/otel_resource.py b/langfuse/api/opentelemetry/types/otel_resource.py new file mode 100644 index 000000000..c8f1bf2b0 --- /dev/null +++ b/langfuse/api/opentelemetry/types/otel_resource.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .otel_attribute import OtelAttribute + + +class OtelResource(UniversalBaseModel): + """ + Resource attributes identifying the source of telemetry + """ + + attributes: typing.Optional[typing.List[OtelAttribute]] = pydantic.Field( + default=None + ) + """ + Resource attributes like service.name, service.version, etc. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/opentelemetry/types/otel_resource_span.py b/langfuse/api/opentelemetry/types/otel_resource_span.py new file mode 100644 index 000000000..d26404cd3 --- /dev/null +++ b/langfuse/api/opentelemetry/types/otel_resource_span.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .otel_resource import OtelResource +from .otel_scope_span import OtelScopeSpan + + +class OtelResourceSpan(UniversalBaseModel): + """ + Represents a collection of spans from a single resource as per OTLP specification + """ + + resource: typing.Optional[OtelResource] = pydantic.Field(default=None) + """ + Resource information + """ + + scope_spans: typing_extensions.Annotated[ + typing.Optional[typing.List[OtelScopeSpan]], FieldMetadata(alias="scopeSpans") + ] = pydantic.Field(default=None) + """ + Array of scope spans + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/opentelemetry/types/otel_scope.py b/langfuse/api/opentelemetry/types/otel_scope.py new file mode 100644 index 000000000..a705fc2ec --- /dev/null +++ b/langfuse/api/opentelemetry/types/otel_scope.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .otel_attribute import OtelAttribute + + +class OtelScope(UniversalBaseModel): + """ + Instrumentation scope information + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Instrumentation scope name + """ + + version: typing.Optional[str] = pydantic.Field(default=None) + """ + Instrumentation scope version + """ + + attributes: typing.Optional[typing.List[OtelAttribute]] = pydantic.Field( + default=None + ) + """ + Additional scope attributes + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/opentelemetry/types/otel_scope_span.py b/langfuse/api/opentelemetry/types/otel_scope_span.py new file mode 100644 index 000000000..9736454b5 --- /dev/null +++ b/langfuse/api/opentelemetry/types/otel_scope_span.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .otel_scope import OtelScope +from .otel_span import OtelSpan + + +class OtelScopeSpan(UniversalBaseModel): + """ + Collection of spans from a single instrumentation scope + """ + + scope: typing.Optional[OtelScope] = pydantic.Field(default=None) + """ + Instrumentation scope information + """ + + spans: typing.Optional[typing.List[OtelSpan]] = pydantic.Field(default=None) + """ + Array of spans + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/opentelemetry/types/otel_span.py b/langfuse/api/opentelemetry/types/otel_span.py new file mode 100644 index 000000000..f6ef51a36 --- /dev/null +++ b/langfuse/api/opentelemetry/types/otel_span.py @@ -0,0 +1,76 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .otel_attribute import OtelAttribute + + +class OtelSpan(UniversalBaseModel): + """ + Individual span representing a unit of work or operation + """ + + trace_id: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="traceId") + ] = pydantic.Field(default=None) + """ + Trace ID (16 bytes, hex-encoded string in JSON or Buffer in binary) + """ + + span_id: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="spanId") + ] = pydantic.Field(default=None) + """ + Span ID (8 bytes, hex-encoded string in JSON or Buffer in binary) + """ + + parent_span_id: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="parentSpanId") + ] = pydantic.Field(default=None) + """ + Parent span ID if this is a child span + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Span name describing the operation + """ + + kind: typing.Optional[int] = pydantic.Field(default=None) + """ + Span kind (1=INTERNAL, 2=SERVER, 3=CLIENT, 4=PRODUCER, 5=CONSUMER) + """ + + start_time_unix_nano: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="startTimeUnixNano") + ] = pydantic.Field(default=None) + """ + Start time in nanoseconds since Unix epoch + """ + + end_time_unix_nano: typing_extensions.Annotated[ + typing.Optional[typing.Any], FieldMetadata(alias="endTimeUnixNano") + ] = pydantic.Field(default=None) + """ + End time in nanoseconds since Unix epoch + """ + + attributes: typing.Optional[typing.List[OtelAttribute]] = pydantic.Field( + default=None + ) + """ + Span attributes including Langfuse-specific attributes (langfuse.observation.*) + """ + + status: typing.Optional[typing.Any] = pydantic.Field(default=None) + """ + Span status object + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/opentelemetry/types/otel_trace_response.py b/langfuse/api/opentelemetry/types/otel_trace_response.py new file mode 100644 index 000000000..02386b088 --- /dev/null +++ b/langfuse/api/opentelemetry/types/otel_trace_response.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class OtelTraceResponse(UniversalBaseModel): + """ + Response from trace export request. Empty object indicates success. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/organizations/__init__.py b/langfuse/api/organizations/__init__.py new file mode 100644 index 000000000..469d10a42 --- /dev/null +++ b/langfuse/api/organizations/__init__.py @@ -0,0 +1,73 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + DeleteMembershipRequest, + MembershipDeletionResponse, + MembershipRequest, + MembershipResponse, + MembershipRole, + MembershipsResponse, + OrganizationApiKey, + OrganizationApiKeysResponse, + OrganizationProject, + OrganizationProjectsResponse, + ) +_dynamic_imports: typing.Dict[str, str] = { + "DeleteMembershipRequest": ".types", + "MembershipDeletionResponse": ".types", + "MembershipRequest": ".types", + "MembershipResponse": ".types", + "MembershipRole": ".types", + "MembershipsResponse": ".types", + "OrganizationApiKey": ".types", + "OrganizationApiKeysResponse": ".types", + "OrganizationProject": ".types", + "OrganizationProjectsResponse": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "DeleteMembershipRequest", + "MembershipDeletionResponse", + "MembershipRequest", + "MembershipResponse", + "MembershipRole", + "MembershipsResponse", + "OrganizationApiKey", + "OrganizationApiKeysResponse", + "OrganizationProject", + "OrganizationProjectsResponse", +] diff --git a/langfuse/api/organizations/client.py b/langfuse/api/organizations/client.py new file mode 100644 index 000000000..085c14c5f --- /dev/null +++ b/langfuse/api/organizations/client.py @@ -0,0 +1,756 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawOrganizationsClient, RawOrganizationsClient +from .types.membership_deletion_response import MembershipDeletionResponse +from .types.membership_response import MembershipResponse +from .types.membership_role import MembershipRole +from .types.memberships_response import MembershipsResponse +from .types.organization_api_keys_response import OrganizationApiKeysResponse +from .types.organization_projects_response import OrganizationProjectsResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class OrganizationsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawOrganizationsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawOrganizationsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawOrganizationsClient + """ + return self._raw_client + + def get_organization_memberships( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> MembershipsResponse: + """ + Get all memberships for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipsResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.get_organization_memberships() + """ + _response = self._raw_client.get_organization_memberships( + request_options=request_options + ) + return _response.data + + def update_organization_membership( + self, + *, + user_id: str, + role: MembershipRole, + request_options: typing.Optional[RequestOptions] = None, + ) -> MembershipResponse: + """ + Create or update a membership for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + role : MembershipRole + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipResponse + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.organizations import MembershipRole + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.update_organization_membership( + user_id="userId", + role=MembershipRole.OWNER, + ) + """ + _response = self._raw_client.update_organization_membership( + user_id=user_id, role=role, request_options=request_options + ) + return _response.data + + def delete_organization_membership( + self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> MembershipDeletionResponse: + """ + Delete a membership from the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipDeletionResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.delete_organization_membership( + user_id="userId", + ) + """ + _response = self._raw_client.delete_organization_membership( + user_id=user_id, request_options=request_options + ) + return _response.data + + def get_project_memberships( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> MembershipsResponse: + """ + Get all memberships for a specific project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipsResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.get_project_memberships( + project_id="projectId", + ) + """ + _response = self._raw_client.get_project_memberships( + project_id, request_options=request_options + ) + return _response.data + + def update_project_membership( + self, + project_id: str, + *, + user_id: str, + role: MembershipRole, + request_options: typing.Optional[RequestOptions] = None, + ) -> MembershipResponse: + """ + Create or update a membership for a specific project (requires organization-scoped API key). The user must already be a member of the organization. + + Parameters + ---------- + project_id : str + + user_id : str + + role : MembershipRole + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipResponse + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.organizations import MembershipRole + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.update_project_membership( + project_id="projectId", + user_id="userId", + role=MembershipRole.OWNER, + ) + """ + _response = self._raw_client.update_project_membership( + project_id, user_id=user_id, role=role, request_options=request_options + ) + return _response.data + + def delete_project_membership( + self, + project_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> MembershipDeletionResponse: + """ + Delete a membership from a specific project (requires organization-scoped API key). The user must be a member of the organization. + + Parameters + ---------- + project_id : str + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipDeletionResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.delete_project_membership( + project_id="projectId", + user_id="userId", + ) + """ + _response = self._raw_client.delete_project_membership( + project_id, user_id=user_id, request_options=request_options + ) + return _response.data + + def get_organization_projects( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> OrganizationProjectsResponse: + """ + Get all projects for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + OrganizationProjectsResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.get_organization_projects() + """ + _response = self._raw_client.get_organization_projects( + request_options=request_options + ) + return _response.data + + def get_organization_api_keys( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> OrganizationApiKeysResponse: + """ + Get all API keys for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + OrganizationApiKeysResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.get_organization_api_keys() + """ + _response = self._raw_client.get_organization_api_keys( + request_options=request_options + ) + return _response.data + + +class AsyncOrganizationsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawOrganizationsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawOrganizationsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawOrganizationsClient + """ + return self._raw_client + + async def get_organization_memberships( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> MembershipsResponse: + """ + Get all memberships for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipsResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.get_organization_memberships() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_organization_memberships( + request_options=request_options + ) + return _response.data + + async def update_organization_membership( + self, + *, + user_id: str, + role: MembershipRole, + request_options: typing.Optional[RequestOptions] = None, + ) -> MembershipResponse: + """ + Create or update a membership for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + role : MembershipRole + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.organizations import MembershipRole + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.update_organization_membership( + user_id="userId", + role=MembershipRole.OWNER, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_organization_membership( + user_id=user_id, role=role, request_options=request_options + ) + return _response.data + + async def delete_organization_membership( + self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> MembershipDeletionResponse: + """ + Delete a membership from the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipDeletionResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.delete_organization_membership( + user_id="userId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_organization_membership( + user_id=user_id, request_options=request_options + ) + return _response.data + + async def get_project_memberships( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> MembershipsResponse: + """ + Get all memberships for a specific project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipsResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.get_project_memberships( + project_id="projectId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_project_memberships( + project_id, request_options=request_options + ) + return _response.data + + async def update_project_membership( + self, + project_id: str, + *, + user_id: str, + role: MembershipRole, + request_options: typing.Optional[RequestOptions] = None, + ) -> MembershipResponse: + """ + Create or update a membership for a specific project (requires organization-scoped API key). The user must already be a member of the organization. + + Parameters + ---------- + project_id : str + + user_id : str + + role : MembershipRole + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.organizations import MembershipRole + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.update_project_membership( + project_id="projectId", + user_id="userId", + role=MembershipRole.OWNER, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_project_membership( + project_id, user_id=user_id, role=role, request_options=request_options + ) + return _response.data + + async def delete_project_membership( + self, + project_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> MembershipDeletionResponse: + """ + Delete a membership from a specific project (requires organization-scoped API key). The user must be a member of the organization. + + Parameters + ---------- + project_id : str + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MembershipDeletionResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.delete_project_membership( + project_id="projectId", + user_id="userId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_project_membership( + project_id, user_id=user_id, request_options=request_options + ) + return _response.data + + async def get_organization_projects( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> OrganizationProjectsResponse: + """ + Get all projects for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + OrganizationProjectsResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.get_organization_projects() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_organization_projects( + request_options=request_options + ) + return _response.data + + async def get_organization_api_keys( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> OrganizationApiKeysResponse: + """ + Get all API keys for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + OrganizationApiKeysResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.get_organization_api_keys() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_organization_api_keys( + request_options=request_options + ) + return _response.data diff --git a/langfuse/api/organizations/raw_client.py b/langfuse/api/organizations/raw_client.py new file mode 100644 index 000000000..89596e8bf --- /dev/null +++ b/langfuse/api/organizations/raw_client.py @@ -0,0 +1,1707 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.membership_deletion_response import MembershipDeletionResponse +from .types.membership_response import MembershipResponse +from .types.membership_role import MembershipRole +from .types.memberships_response import MembershipsResponse +from .types.organization_api_keys_response import OrganizationApiKeysResponse +from .types.organization_projects_response import OrganizationProjectsResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawOrganizationsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_organization_memberships( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[MembershipsResponse]: + """ + Get all memberships for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MembershipsResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/organizations/memberships", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipsResponse, + parse_obj_as( + type_=MembershipsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def update_organization_membership( + self, + *, + user_id: str, + role: MembershipRole, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[MembershipResponse]: + """ + Create or update a membership for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + role : MembershipRole + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MembershipResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/organizations/memberships", + method="PUT", + json={ + "userId": user_id, + "role": role, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipResponse, + parse_obj_as( + type_=MembershipResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_organization_membership( + self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[MembershipDeletionResponse]: + """ + Delete a membership from the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MembershipDeletionResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/organizations/memberships", + method="DELETE", + json={ + "userId": user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipDeletionResponse, + parse_obj_as( + type_=MembershipDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_project_memberships( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[MembershipsResponse]: + """ + Get all memberships for a specific project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MembershipsResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/memberships", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipsResponse, + parse_obj_as( + type_=MembershipsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def update_project_membership( + self, + project_id: str, + *, + user_id: str, + role: MembershipRole, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[MembershipResponse]: + """ + Create or update a membership for a specific project (requires organization-scoped API key). The user must already be a member of the organization. + + Parameters + ---------- + project_id : str + + user_id : str + + role : MembershipRole + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MembershipResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/memberships", + method="PUT", + json={ + "userId": user_id, + "role": role, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipResponse, + parse_obj_as( + type_=MembershipResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_project_membership( + self, + project_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[MembershipDeletionResponse]: + """ + Delete a membership from a specific project (requires organization-scoped API key). The user must be a member of the organization. + + Parameters + ---------- + project_id : str + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MembershipDeletionResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/memberships", + method="DELETE", + json={ + "userId": user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipDeletionResponse, + parse_obj_as( + type_=MembershipDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_organization_projects( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[OrganizationProjectsResponse]: + """ + Get all projects for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[OrganizationProjectsResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/organizations/projects", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + OrganizationProjectsResponse, + parse_obj_as( + type_=OrganizationProjectsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_organization_api_keys( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[OrganizationApiKeysResponse]: + """ + Get all API keys for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[OrganizationApiKeysResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/organizations/apiKeys", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + OrganizationApiKeysResponse, + parse_obj_as( + type_=OrganizationApiKeysResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawOrganizationsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_organization_memberships( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[MembershipsResponse]: + """ + Get all memberships for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MembershipsResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/organizations/memberships", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipsResponse, + parse_obj_as( + type_=MembershipsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def update_organization_membership( + self, + *, + user_id: str, + role: MembershipRole, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[MembershipResponse]: + """ + Create or update a membership for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + role : MembershipRole + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MembershipResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/organizations/memberships", + method="PUT", + json={ + "userId": user_id, + "role": role, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipResponse, + parse_obj_as( + type_=MembershipResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_organization_membership( + self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[MembershipDeletionResponse]: + """ + Delete a membership from the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MembershipDeletionResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/organizations/memberships", + method="DELETE", + json={ + "userId": user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipDeletionResponse, + parse_obj_as( + type_=MembershipDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_project_memberships( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[MembershipsResponse]: + """ + Get all memberships for a specific project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MembershipsResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/memberships", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipsResponse, + parse_obj_as( + type_=MembershipsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def update_project_membership( + self, + project_id: str, + *, + user_id: str, + role: MembershipRole, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[MembershipResponse]: + """ + Create or update a membership for a specific project (requires organization-scoped API key). The user must already be a member of the organization. + + Parameters + ---------- + project_id : str + + user_id : str + + role : MembershipRole + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MembershipResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/memberships", + method="PUT", + json={ + "userId": user_id, + "role": role, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipResponse, + parse_obj_as( + type_=MembershipResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_project_membership( + self, + project_id: str, + *, + user_id: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[MembershipDeletionResponse]: + """ + Delete a membership from a specific project (requires organization-scoped API key). The user must be a member of the organization. + + Parameters + ---------- + project_id : str + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MembershipDeletionResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/memberships", + method="DELETE", + json={ + "userId": user_id, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MembershipDeletionResponse, + parse_obj_as( + type_=MembershipDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_organization_projects( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[OrganizationProjectsResponse]: + """ + Get all projects for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[OrganizationProjectsResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/organizations/projects", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + OrganizationProjectsResponse, + parse_obj_as( + type_=OrganizationProjectsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_organization_api_keys( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[OrganizationApiKeysResponse]: + """ + Get all API keys for the organization associated with the API key (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[OrganizationApiKeysResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/organizations/apiKeys", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + OrganizationApiKeysResponse, + parse_obj_as( + type_=OrganizationApiKeysResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/organizations/types/__init__.py b/langfuse/api/organizations/types/__init__.py new file mode 100644 index 000000000..2ac6cb3e7 --- /dev/null +++ b/langfuse/api/organizations/types/__init__.py @@ -0,0 +1,71 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .delete_membership_request import DeleteMembershipRequest + from .membership_deletion_response import MembershipDeletionResponse + from .membership_request import MembershipRequest + from .membership_response import MembershipResponse + from .membership_role import MembershipRole + from .memberships_response import MembershipsResponse + from .organization_api_key import OrganizationApiKey + from .organization_api_keys_response import OrganizationApiKeysResponse + from .organization_project import OrganizationProject + from .organization_projects_response import OrganizationProjectsResponse +_dynamic_imports: typing.Dict[str, str] = { + "DeleteMembershipRequest": ".delete_membership_request", + "MembershipDeletionResponse": ".membership_deletion_response", + "MembershipRequest": ".membership_request", + "MembershipResponse": ".membership_response", + "MembershipRole": ".membership_role", + "MembershipsResponse": ".memberships_response", + "OrganizationApiKey": ".organization_api_key", + "OrganizationApiKeysResponse": ".organization_api_keys_response", + "OrganizationProject": ".organization_project", + "OrganizationProjectsResponse": ".organization_projects_response", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "DeleteMembershipRequest", + "MembershipDeletionResponse", + "MembershipRequest", + "MembershipResponse", + "MembershipRole", + "MembershipsResponse", + "OrganizationApiKey", + "OrganizationApiKeysResponse", + "OrganizationProject", + "OrganizationProjectsResponse", +] diff --git a/langfuse/api/organizations/types/delete_membership_request.py b/langfuse/api/organizations/types/delete_membership_request.py new file mode 100644 index 000000000..a48c85283 --- /dev/null +++ b/langfuse/api/organizations/types/delete_membership_request.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class DeleteMembershipRequest(UniversalBaseModel): + user_id: typing_extensions.Annotated[str, FieldMetadata(alias="userId")] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/organizations/types/membership_deletion_response.py b/langfuse/api/organizations/types/membership_deletion_response.py new file mode 100644 index 000000000..b1c0c3940 --- /dev/null +++ b/langfuse/api/organizations/types/membership_deletion_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class MembershipDeletionResponse(UniversalBaseModel): + message: str + user_id: typing_extensions.Annotated[str, FieldMetadata(alias="userId")] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/organizations/types/membership_request.py b/langfuse/api/organizations/types/membership_request.py new file mode 100644 index 000000000..c1edbebdd --- /dev/null +++ b/langfuse/api/organizations/types/membership_request.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .membership_role import MembershipRole + + +class MembershipRequest(UniversalBaseModel): + user_id: typing_extensions.Annotated[str, FieldMetadata(alias="userId")] + role: MembershipRole + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/organizations/types/membership_response.py b/langfuse/api/organizations/types/membership_response.py new file mode 100644 index 000000000..24074c370 --- /dev/null +++ b/langfuse/api/organizations/types/membership_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .membership_role import MembershipRole + + +class MembershipResponse(UniversalBaseModel): + user_id: typing_extensions.Annotated[str, FieldMetadata(alias="userId")] + role: MembershipRole + email: str + name: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/resources/organizations/types/membership_role.py b/langfuse/api/organizations/types/membership_role.py similarity index 92% rename from langfuse/api/resources/organizations/types/membership_role.py rename to langfuse/api/organizations/types/membership_role.py index 1721cc0ed..fa84eec3a 100644 --- a/langfuse/api/resources/organizations/types/membership_role.py +++ b/langfuse/api/organizations/types/membership_role.py @@ -1,12 +1,13 @@ # This file was auto-generated by Fern from our API Definition. -import enum import typing +from ...core import enum + T_Result = typing.TypeVar("T_Result") -class MembershipRole(str, enum.Enum): +class MembershipRole(enum.StrEnum): OWNER = "OWNER" ADMIN = "ADMIN" MEMBER = "MEMBER" diff --git a/langfuse/api/organizations/types/memberships_response.py b/langfuse/api/organizations/types/memberships_response.py new file mode 100644 index 000000000..f45dc9942 --- /dev/null +++ b/langfuse/api/organizations/types/memberships_response.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .membership_response import MembershipResponse + + +class MembershipsResponse(UniversalBaseModel): + memberships: typing.List[MembershipResponse] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/organizations/types/organization_api_key.py b/langfuse/api/organizations/types/organization_api_key.py new file mode 100644 index 000000000..572015ab3 --- /dev/null +++ b/langfuse/api/organizations/types/organization_api_key.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class OrganizationApiKey(UniversalBaseModel): + id: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + expires_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="expiresAt") + ] = None + last_used_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="lastUsedAt") + ] = None + note: typing.Optional[str] = None + public_key: typing_extensions.Annotated[str, FieldMetadata(alias="publicKey")] + display_secret_key: typing_extensions.Annotated[ + str, FieldMetadata(alias="displaySecretKey") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/organizations/types/organization_api_keys_response.py b/langfuse/api/organizations/types/organization_api_keys_response.py new file mode 100644 index 000000000..f0ca789da --- /dev/null +++ b/langfuse/api/organizations/types/organization_api_keys_response.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .organization_api_key import OrganizationApiKey + + +class OrganizationApiKeysResponse(UniversalBaseModel): + api_keys: typing_extensions.Annotated[ + typing.List[OrganizationApiKey], FieldMetadata(alias="apiKeys") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/organizations/types/organization_project.py b/langfuse/api/organizations/types/organization_project.py new file mode 100644 index 000000000..9df0fe961 --- /dev/null +++ b/langfuse/api/organizations/types/organization_project.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class OrganizationProject(UniversalBaseModel): + id: str + name: str + metadata: typing.Optional[typing.Dict[str, typing.Any]] = None + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/organizations/types/organization_projects_response.py b/langfuse/api/organizations/types/organization_projects_response.py new file mode 100644 index 000000000..e70925ae0 --- /dev/null +++ b/langfuse/api/organizations/types/organization_projects_response.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .organization_project import OrganizationProject + + +class OrganizationProjectsResponse(UniversalBaseModel): + projects: typing.List[OrganizationProject] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/projects/__init__.py b/langfuse/api/projects/__init__.py new file mode 100644 index 000000000..1eb633f7b --- /dev/null +++ b/langfuse/api/projects/__init__.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + ApiKeyDeletionResponse, + ApiKeyList, + ApiKeyResponse, + ApiKeySummary, + Organization, + Project, + ProjectDeletionResponse, + Projects, + ) +_dynamic_imports: typing.Dict[str, str] = { + "ApiKeyDeletionResponse": ".types", + "ApiKeyList": ".types", + "ApiKeyResponse": ".types", + "ApiKeySummary": ".types", + "Organization": ".types", + "Project": ".types", + "ProjectDeletionResponse": ".types", + "Projects": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ApiKeyDeletionResponse", + "ApiKeyList", + "ApiKeyResponse", + "ApiKeySummary", + "Organization", + "Project", + "ProjectDeletionResponse", + "Projects", +] diff --git a/langfuse/api/projects/client.py b/langfuse/api/projects/client.py new file mode 100644 index 000000000..52350f3b6 --- /dev/null +++ b/langfuse/api/projects/client.py @@ -0,0 +1,760 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawProjectsClient, RawProjectsClient +from .types.api_key_deletion_response import ApiKeyDeletionResponse +from .types.api_key_list import ApiKeyList +from .types.api_key_response import ApiKeyResponse +from .types.project import Project +from .types.project_deletion_response import ProjectDeletionResponse +from .types.projects import Projects + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ProjectsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawProjectsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawProjectsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawProjectsClient + """ + return self._raw_client + + def get( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> Projects: + """ + Get Project associated with API key (requires project-scoped API key). You can use GET /api/public/organizations/projects to get all projects with an organization-scoped key. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Projects + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.projects.get() + """ + _response = self._raw_client.get(request_options=request_options) + return _response.data + + def create( + self, + *, + name: str, + retention: int, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Project: + """ + Create a new project (requires organization-scoped API key) + + Parameters + ---------- + name : str + + retention : int + Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + Optional metadata for the project + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Project + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.projects.create( + name="name", + retention=1, + ) + """ + _response = self._raw_client.create( + name=name, + retention=retention, + metadata=metadata, + request_options=request_options, + ) + return _response.data + + def update( + self, + project_id: str, + *, + name: str, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + retention: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Project: + """ + Update a project by ID (requires organization-scoped API key). + + Parameters + ---------- + project_id : str + + name : str + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + Optional metadata for the project + + retention : typing.Optional[int] + Number of days to retain data. + Must be 0 or at least 3 days. + Requires data-retention entitlement for non-zero values. + Optional. Will retain existing retention setting if omitted. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Project + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.projects.update( + project_id="projectId", + name="name", + ) + """ + _response = self._raw_client.update( + project_id, + name=name, + metadata=metadata, + retention=retention, + request_options=request_options, + ) + return _response.data + + def delete( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> ProjectDeletionResponse: + """ + Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously. + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProjectDeletionResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.projects.delete( + project_id="projectId", + ) + """ + _response = self._raw_client.delete(project_id, request_options=request_options) + return _response.data + + def get_api_keys( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> ApiKeyList: + """ + Get all API keys for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ApiKeyList + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.projects.get_api_keys( + project_id="projectId", + ) + """ + _response = self._raw_client.get_api_keys( + project_id, request_options=request_options + ) + return _response.data + + def create_api_key( + self, + project_id: str, + *, + note: typing.Optional[str] = OMIT, + public_key: typing.Optional[str] = OMIT, + secret_key: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> ApiKeyResponse: + """ + Create a new API key for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + note : typing.Optional[str] + Optional note for the API key + + public_key : typing.Optional[str] + Optional predefined public key. Must start with 'pk-lf-'. If provided, secretKey must also be provided. + + secret_key : typing.Optional[str] + Optional predefined secret key. Must start with 'sk-lf-'. If provided, publicKey must also be provided. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ApiKeyResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.projects.create_api_key( + project_id="projectId", + ) + """ + _response = self._raw_client.create_api_key( + project_id, + note=note, + public_key=public_key, + secret_key=secret_key, + request_options=request_options, + ) + return _response.data + + def delete_api_key( + self, + project_id: str, + api_key_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> ApiKeyDeletionResponse: + """ + Delete an API key for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + api_key_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ApiKeyDeletionResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.projects.delete_api_key( + project_id="projectId", + api_key_id="apiKeyId", + ) + """ + _response = self._raw_client.delete_api_key( + project_id, api_key_id, request_options=request_options + ) + return _response.data + + +class AsyncProjectsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawProjectsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawProjectsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawProjectsClient + """ + return self._raw_client + + async def get( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> Projects: + """ + Get Project associated with API key (requires project-scoped API key). You can use GET /api/public/organizations/projects to get all projects with an organization-scoped key. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Projects + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.projects.get() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(request_options=request_options) + return _response.data + + async def create( + self, + *, + name: str, + retention: int, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Project: + """ + Create a new project (requires organization-scoped API key) + + Parameters + ---------- + name : str + + retention : int + Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + Optional metadata for the project + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Project + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.projects.create( + name="name", + retention=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + name=name, + retention=retention, + metadata=metadata, + request_options=request_options, + ) + return _response.data + + async def update( + self, + project_id: str, + *, + name: str, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + retention: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> Project: + """ + Update a project by ID (requires organization-scoped API key). + + Parameters + ---------- + project_id : str + + name : str + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + Optional metadata for the project + + retention : typing.Optional[int] + Number of days to retain data. + Must be 0 or at least 3 days. + Requires data-retention entitlement for non-zero values. + Optional. Will retain existing retention setting if omitted. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Project + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.projects.update( + project_id="projectId", + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + project_id, + name=name, + metadata=metadata, + retention=retention, + request_options=request_options, + ) + return _response.data + + async def delete( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> ProjectDeletionResponse: + """ + Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously. + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProjectDeletionResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.projects.delete( + project_id="projectId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete( + project_id, request_options=request_options + ) + return _response.data + + async def get_api_keys( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> ApiKeyList: + """ + Get all API keys for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ApiKeyList + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.projects.get_api_keys( + project_id="projectId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_api_keys( + project_id, request_options=request_options + ) + return _response.data + + async def create_api_key( + self, + project_id: str, + *, + note: typing.Optional[str] = OMIT, + public_key: typing.Optional[str] = OMIT, + secret_key: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> ApiKeyResponse: + """ + Create a new API key for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + note : typing.Optional[str] + Optional note for the API key + + public_key : typing.Optional[str] + Optional predefined public key. Must start with 'pk-lf-'. If provided, secretKey must also be provided. + + secret_key : typing.Optional[str] + Optional predefined secret key. Must start with 'sk-lf-'. If provided, publicKey must also be provided. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ApiKeyResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.projects.create_api_key( + project_id="projectId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_api_key( + project_id, + note=note, + public_key=public_key, + secret_key=secret_key, + request_options=request_options, + ) + return _response.data + + async def delete_api_key( + self, + project_id: str, + api_key_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> ApiKeyDeletionResponse: + """ + Delete an API key for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + api_key_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ApiKeyDeletionResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.projects.delete_api_key( + project_id="projectId", + api_key_id="apiKeyId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_api_key( + project_id, api_key_id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/projects/raw_client.py b/langfuse/api/projects/raw_client.py new file mode 100644 index 000000000..524f37d3f --- /dev/null +++ b/langfuse/api/projects/raw_client.py @@ -0,0 +1,1577 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.api_key_deletion_response import ApiKeyDeletionResponse +from .types.api_key_list import ApiKeyList +from .types.api_key_response import ApiKeyResponse +from .types.project import Project +from .types.project_deletion_response import ProjectDeletionResponse +from .types.projects import Projects + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawProjectsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[Projects]: + """ + Get Project associated with API key (requires project-scoped API key). You can use GET /api/public/organizations/projects to get all projects with an organization-scoped key. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Projects] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/projects", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Projects, + parse_obj_as( + type_=Projects, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create( + self, + *, + name: str, + retention: int, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Project]: + """ + Create a new project (requires organization-scoped API key) + + Parameters + ---------- + name : str + + retention : int + Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + Optional metadata for the project + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Project] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/projects", + method="POST", + json={ + "name": name, + "metadata": metadata, + "retention": retention, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Project, + parse_obj_as( + type_=Project, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def update( + self, + project_id: str, + *, + name: str, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + retention: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Project]: + """ + Update a project by ID (requires organization-scoped API key). + + Parameters + ---------- + project_id : str + + name : str + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + Optional metadata for the project + + retention : typing.Optional[int] + Number of days to retain data. + Must be 0 or at least 3 days. + Requires data-retention entitlement for non-zero values. + Optional. Will retain existing retention setting if omitted. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Project] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}", + method="PUT", + json={ + "name": name, + "metadata": metadata, + "retention": retention, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Project, + parse_obj_as( + type_=Project, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ProjectDeletionResponse]: + """ + Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously. + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProjectDeletionResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProjectDeletionResponse, + parse_obj_as( + type_=ProjectDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_api_keys( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ApiKeyList]: + """ + Get all API keys for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ApiKeyList] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ApiKeyList, + parse_obj_as( + type_=ApiKeyList, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create_api_key( + self, + project_id: str, + *, + note: typing.Optional[str] = OMIT, + public_key: typing.Optional[str] = OMIT, + secret_key: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ApiKeyResponse]: + """ + Create a new API key for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + note : typing.Optional[str] + Optional note for the API key + + public_key : typing.Optional[str] + Optional predefined public key. Must start with 'pk-lf-'. If provided, secretKey must also be provided. + + secret_key : typing.Optional[str] + Optional predefined secret key. Must start with 'sk-lf-'. If provided, publicKey must also be provided. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ApiKeyResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", + method="POST", + json={ + "note": note, + "publicKey": public_key, + "secretKey": secret_key, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ApiKeyResponse, + parse_obj_as( + type_=ApiKeyResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_api_key( + self, + project_id: str, + api_key_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ApiKeyDeletionResponse]: + """ + Delete an API key for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + api_key_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ApiKeyDeletionResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys/{jsonable_encoder(api_key_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ApiKeyDeletionResponse, + parse_obj_as( + type_=ApiKeyDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawProjectsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[Projects]: + """ + Get Project associated with API key (requires project-scoped API key). You can use GET /api/public/organizations/projects to get all projects with an organization-scoped key. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Projects] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/projects", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Projects, + parse_obj_as( + type_=Projects, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create( + self, + *, + name: str, + retention: int, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Project]: + """ + Create a new project (requires organization-scoped API key) + + Parameters + ---------- + name : str + + retention : int + Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + Optional metadata for the project + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Project] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/projects", + method="POST", + json={ + "name": name, + "metadata": metadata, + "retention": retention, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Project, + parse_obj_as( + type_=Project, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def update( + self, + project_id: str, + *, + name: str, + metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + retention: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Project]: + """ + Update a project by ID (requires organization-scoped API key). + + Parameters + ---------- + project_id : str + + name : str + + metadata : typing.Optional[typing.Dict[str, typing.Any]] + Optional metadata for the project + + retention : typing.Optional[int] + Number of days to retain data. + Must be 0 or at least 3 days. + Requires data-retention entitlement for non-zero values. + Optional. Will retain existing retention setting if omitted. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Project] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}", + method="PUT", + json={ + "name": name, + "metadata": metadata, + "retention": retention, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Project, + parse_obj_as( + type_=Project, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ProjectDeletionResponse]: + """ + Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously. + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProjectDeletionResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProjectDeletionResponse, + parse_obj_as( + type_=ProjectDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_api_keys( + self, + project_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ApiKeyList]: + """ + Get all API keys for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ApiKeyList] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ApiKeyList, + parse_obj_as( + type_=ApiKeyList, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create_api_key( + self, + project_id: str, + *, + note: typing.Optional[str] = OMIT, + public_key: typing.Optional[str] = OMIT, + secret_key: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ApiKeyResponse]: + """ + Create a new API key for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + note : typing.Optional[str] + Optional note for the API key + + public_key : typing.Optional[str] + Optional predefined public key. Must start with 'pk-lf-'. If provided, secretKey must also be provided. + + secret_key : typing.Optional[str] + Optional predefined secret key. Must start with 'sk-lf-'. If provided, publicKey must also be provided. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ApiKeyResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", + method="POST", + json={ + "note": note, + "publicKey": public_key, + "secretKey": secret_key, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ApiKeyResponse, + parse_obj_as( + type_=ApiKeyResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_api_key( + self, + project_id: str, + api_key_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ApiKeyDeletionResponse]: + """ + Delete an API key for a project (requires organization-scoped API key) + + Parameters + ---------- + project_id : str + + api_key_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ApiKeyDeletionResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys/{jsonable_encoder(api_key_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ApiKeyDeletionResponse, + parse_obj_as( + type_=ApiKeyDeletionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/projects/types/__init__.py b/langfuse/api/projects/types/__init__.py new file mode 100644 index 000000000..348f521c0 --- /dev/null +++ b/langfuse/api/projects/types/__init__.py @@ -0,0 +1,65 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .api_key_deletion_response import ApiKeyDeletionResponse + from .api_key_list import ApiKeyList + from .api_key_response import ApiKeyResponse + from .api_key_summary import ApiKeySummary + from .organization import Organization + from .project import Project + from .project_deletion_response import ProjectDeletionResponse + from .projects import Projects +_dynamic_imports: typing.Dict[str, str] = { + "ApiKeyDeletionResponse": ".api_key_deletion_response", + "ApiKeyList": ".api_key_list", + "ApiKeyResponse": ".api_key_response", + "ApiKeySummary": ".api_key_summary", + "Organization": ".organization", + "Project": ".project", + "ProjectDeletionResponse": ".project_deletion_response", + "Projects": ".projects", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ApiKeyDeletionResponse", + "ApiKeyList", + "ApiKeyResponse", + "ApiKeySummary", + "Organization", + "Project", + "ProjectDeletionResponse", + "Projects", +] diff --git a/langfuse/api/projects/types/api_key_deletion_response.py b/langfuse/api/projects/types/api_key_deletion_response.py new file mode 100644 index 000000000..fd6a69448 --- /dev/null +++ b/langfuse/api/projects/types/api_key_deletion_response.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ApiKeyDeletionResponse(UniversalBaseModel): + """ + Response for API key deletion + """ + + success: bool + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/projects/types/api_key_list.py b/langfuse/api/projects/types/api_key_list.py new file mode 100644 index 000000000..bf38d9be1 --- /dev/null +++ b/langfuse/api/projects/types/api_key_list.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .api_key_summary import ApiKeySummary + + +class ApiKeyList(UniversalBaseModel): + """ + List of API keys for a project + """ + + api_keys: typing_extensions.Annotated[ + typing.List[ApiKeySummary], FieldMetadata(alias="apiKeys") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/projects/types/api_key_response.py b/langfuse/api/projects/types/api_key_response.py new file mode 100644 index 000000000..06ad54610 --- /dev/null +++ b/langfuse/api/projects/types/api_key_response.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class ApiKeyResponse(UniversalBaseModel): + """ + Response for API key creation + """ + + id: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + public_key: typing_extensions.Annotated[str, FieldMetadata(alias="publicKey")] + secret_key: typing_extensions.Annotated[str, FieldMetadata(alias="secretKey")] + display_secret_key: typing_extensions.Annotated[ + str, FieldMetadata(alias="displaySecretKey") + ] + note: typing.Optional[str] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/projects/types/api_key_summary.py b/langfuse/api/projects/types/api_key_summary.py new file mode 100644 index 000000000..68d21421b --- /dev/null +++ b/langfuse/api/projects/types/api_key_summary.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class ApiKeySummary(UniversalBaseModel): + """ + Summary of an API key + """ + + id: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + expires_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="expiresAt") + ] = None + last_used_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="lastUsedAt") + ] = None + note: typing.Optional[str] = None + public_key: typing_extensions.Annotated[str, FieldMetadata(alias="publicKey")] + display_secret_key: typing_extensions.Annotated[ + str, FieldMetadata(alias="displaySecretKey") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/projects/types/organization.py b/langfuse/api/projects/types/organization.py new file mode 100644 index 000000000..c6d1108b3 --- /dev/null +++ b/langfuse/api/projects/types/organization.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class Organization(UniversalBaseModel): + id: str = pydantic.Field() + """ + The unique identifier of the organization + """ + + name: str = pydantic.Field() + """ + The name of the organization + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/projects/types/project.py b/langfuse/api/projects/types/project.py new file mode 100644 index 000000000..b8b83cf7e --- /dev/null +++ b/langfuse/api/projects/types/project.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .organization import Organization + + +class Project(UniversalBaseModel): + id: str + name: str + organization: Organization = pydantic.Field() + """ + The organization this project belongs to + """ + + metadata: typing.Dict[str, typing.Any] = pydantic.Field() + """ + Metadata for the project + """ + + retention_days: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="retentionDays") + ] = pydantic.Field(default=None) + """ + Number of days to retain data. Null or 0 means no retention. Omitted if no retention is configured. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/projects/types/project_deletion_response.py b/langfuse/api/projects/types/project_deletion_response.py new file mode 100644 index 000000000..e3d471c78 --- /dev/null +++ b/langfuse/api/projects/types/project_deletion_response.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ProjectDeletionResponse(UniversalBaseModel): + success: bool + message: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/projects/types/projects.py b/langfuse/api/projects/types/projects.py new file mode 100644 index 000000000..5771c0051 --- /dev/null +++ b/langfuse/api/projects/types/projects.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .project import Project + + +class Projects(UniversalBaseModel): + data: typing.List[Project] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/resources/prompt_version/__init__.py b/langfuse/api/prompt_version/__init__.py similarity index 76% rename from langfuse/api/resources/prompt_version/__init__.py rename to langfuse/api/prompt_version/__init__.py index f3ea2659b..5cde0202d 100644 --- a/langfuse/api/resources/prompt_version/__init__.py +++ b/langfuse/api/prompt_version/__init__.py @@ -1,2 +1,4 @@ # This file was auto-generated by Fern from our API Definition. +# isort: skip_file + diff --git a/langfuse/api/prompt_version/client.py b/langfuse/api/prompt_version/client.py new file mode 100644 index 000000000..f212447a4 --- /dev/null +++ b/langfuse/api/prompt_version/client.py @@ -0,0 +1,157 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..prompts.types.prompt import Prompt +from .raw_client import AsyncRawPromptVersionClient, RawPromptVersionClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class PromptVersionClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawPromptVersionClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawPromptVersionClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawPromptVersionClient + """ + return self._raw_client + + def update( + self, + name: str, + version: int, + *, + new_labels: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> Prompt: + """ + Update labels for a specific prompt version + + Parameters + ---------- + name : str + The name of the prompt. If the prompt is in a folder (e.g., "folder/subfolder/prompt-name"), + the folder path must be URL encoded. + + version : int + Version of the prompt to update + + new_labels : typing.Sequence[str] + New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Prompt + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.prompt_version.update( + name="name", + version=1, + new_labels=["newLabels", "newLabels"], + ) + """ + _response = self._raw_client.update( + name, version, new_labels=new_labels, request_options=request_options + ) + return _response.data + + +class AsyncPromptVersionClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawPromptVersionClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawPromptVersionClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawPromptVersionClient + """ + return self._raw_client + + async def update( + self, + name: str, + version: int, + *, + new_labels: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> Prompt: + """ + Update labels for a specific prompt version + + Parameters + ---------- + name : str + The name of the prompt. If the prompt is in a folder (e.g., "folder/subfolder/prompt-name"), + the folder path must be URL encoded. + + version : int + Version of the prompt to update + + new_labels : typing.Sequence[str] + New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Prompt + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.prompt_version.update( + name="name", + version=1, + new_labels=["newLabels", "newLabels"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + name, version, new_labels=new_labels, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/prompt_version/raw_client.py b/langfuse/api/prompt_version/raw_client.py new file mode 100644 index 000000000..7d1e8d3f2 --- /dev/null +++ b/langfuse/api/prompt_version/raw_client.py @@ -0,0 +1,264 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..prompts.types.prompt import Prompt + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawPromptVersionClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def update( + self, + name: str, + version: int, + *, + new_labels: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Prompt]: + """ + Update labels for a specific prompt version + + Parameters + ---------- + name : str + The name of the prompt. If the prompt is in a folder (e.g., "folder/subfolder/prompt-name"), + the folder path must be URL encoded. + + version : int + Version of the prompt to update + + new_labels : typing.Sequence[str] + New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Prompt] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/v2/prompts/{jsonable_encoder(name)}/versions/{jsonable_encoder(version)}", + method="PATCH", + json={ + "newLabels": new_labels, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Prompt, + parse_obj_as( + type_=Prompt, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawPromptVersionClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def update( + self, + name: str, + version: int, + *, + new_labels: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Prompt]: + """ + Update labels for a specific prompt version + + Parameters + ---------- + name : str + The name of the prompt. If the prompt is in a folder (e.g., "folder/subfolder/prompt-name"), + the folder path must be URL encoded. + + version : int + Version of the prompt to update + + new_labels : typing.Sequence[str] + New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Prompt] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/v2/prompts/{jsonable_encoder(name)}/versions/{jsonable_encoder(version)}", + method="PATCH", + json={ + "newLabels": new_labels, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Prompt, + parse_obj_as( + type_=Prompt, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/prompts/__init__.py b/langfuse/api/prompts/__init__.py new file mode 100644 index 000000000..13e115e7b --- /dev/null +++ b/langfuse/api/prompts/__init__.py @@ -0,0 +1,100 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + BasePrompt, + ChatMessage, + ChatMessageType, + ChatMessageWithPlaceholders, + ChatPrompt, + CreateChatPromptRequest, + CreateChatPromptType, + CreatePromptRequest, + CreateTextPromptRequest, + CreateTextPromptType, + PlaceholderMessage, + PlaceholderMessageType, + Prompt, + PromptMeta, + PromptMetaListResponse, + PromptType, + Prompt_Chat, + Prompt_Text, + TextPrompt, + ) +_dynamic_imports: typing.Dict[str, str] = { + "BasePrompt": ".types", + "ChatMessage": ".types", + "ChatMessageType": ".types", + "ChatMessageWithPlaceholders": ".types", + "ChatPrompt": ".types", + "CreateChatPromptRequest": ".types", + "CreateChatPromptType": ".types", + "CreatePromptRequest": ".types", + "CreateTextPromptRequest": ".types", + "CreateTextPromptType": ".types", + "PlaceholderMessage": ".types", + "PlaceholderMessageType": ".types", + "Prompt": ".types", + "PromptMeta": ".types", + "PromptMetaListResponse": ".types", + "PromptType": ".types", + "Prompt_Chat": ".types", + "Prompt_Text": ".types", + "TextPrompt": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BasePrompt", + "ChatMessage", + "ChatMessageType", + "ChatMessageWithPlaceholders", + "ChatPrompt", + "CreateChatPromptRequest", + "CreateChatPromptType", + "CreatePromptRequest", + "CreateTextPromptRequest", + "CreateTextPromptType", + "PlaceholderMessage", + "PlaceholderMessageType", + "Prompt", + "PromptMeta", + "PromptMetaListResponse", + "PromptType", + "Prompt_Chat", + "Prompt_Text", + "TextPrompt", +] diff --git a/langfuse/api/prompts/client.py b/langfuse/api/prompts/client.py new file mode 100644 index 000000000..eff5e572f --- /dev/null +++ b/langfuse/api/prompts/client.py @@ -0,0 +1,550 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawPromptsClient, RawPromptsClient +from .types.create_prompt_request import CreatePromptRequest +from .types.prompt import Prompt +from .types.prompt_meta_list_response import PromptMetaListResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class PromptsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawPromptsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawPromptsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawPromptsClient + """ + return self._raw_client + + def get( + self, + prompt_name: str, + *, + version: typing.Optional[int] = None, + label: typing.Optional[str] = None, + resolve: typing.Optional[bool] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> Prompt: + """ + Get a prompt + + Parameters + ---------- + prompt_name : str + The name of the prompt. If the prompt is in a folder (e.g., "folder/subfolder/prompt-name"), + the folder path must be URL encoded. + + version : typing.Optional[int] + Version of the prompt to be retrieved. + + label : typing.Optional[str] + Label of the prompt to be retrieved. Defaults to "production" if no label or version is set. + + resolve : typing.Optional[bool] + Resolve prompt dependencies before returning the prompt. Defaults to `true`. Set to `false` to return the raw stored prompt with dependency tags intact. This bypasses prompt caching and is intended for debugging or one-off jobs, not production runtime fetches. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Prompt + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.prompts.get( + prompt_name="promptName", + ) + """ + _response = self._raw_client.get( + prompt_name, + version=version, + label=label, + resolve=resolve, + request_options=request_options, + ) + return _response.data + + def list( + self, + *, + name: typing.Optional[str] = None, + label: typing.Optional[str] = None, + tag: typing.Optional[str] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_updated_at: typing.Optional[dt.datetime] = None, + to_updated_at: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptMetaListResponse: + """ + Get a list of prompt names with versions and labels + + Parameters + ---------- + name : typing.Optional[str] + + label : typing.Optional[str] + + tag : typing.Optional[str] + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + from_updated_at : typing.Optional[dt.datetime] + Optional filter to only include prompt versions created/updated on or after a certain datetime (ISO 8601) + + to_updated_at : typing.Optional[dt.datetime] + Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptMetaListResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.prompts.list() + """ + _response = self._raw_client.list( + name=name, + label=label, + tag=tag, + page=page, + limit=limit, + from_updated_at=from_updated_at, + to_updated_at=to_updated_at, + request_options=request_options, + ) + return _response.data + + def create( + self, + *, + request: CreatePromptRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> Prompt: + """ + Create a new version for the prompt with the given `name` + + Parameters + ---------- + request : CreatePromptRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Prompt + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.prompts import ( + ChatMessage, + CreateChatPromptRequest, + CreateChatPromptType, + ) + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.prompts.create( + request=CreateChatPromptRequest( + name="name", + prompt=[ + ChatMessage( + role="role", + content="content", + ), + ChatMessage( + role="role", + content="content", + ), + ], + type=CreateChatPromptType.CHAT, + ), + ) + """ + _response = self._raw_client.create( + request=request, request_options=request_options + ) + return _response.data + + def delete( + self, + prompt_name: str, + *, + label: typing.Optional[str] = None, + version: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> None: + """ + Delete prompt versions. If neither version nor label is specified, all versions of the prompt are deleted. + + Parameters + ---------- + prompt_name : str + The name of the prompt + + label : typing.Optional[str] + Optional label to filter deletion. If specified, deletes all prompt versions that have this label. + + version : typing.Optional[int] + Optional version to filter deletion. If specified, deletes only this specific version of the prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.prompts.delete( + prompt_name="promptName", + ) + """ + _response = self._raw_client.delete( + prompt_name, label=label, version=version, request_options=request_options + ) + return _response.data + + +class AsyncPromptsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawPromptsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawPromptsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawPromptsClient + """ + return self._raw_client + + async def get( + self, + prompt_name: str, + *, + version: typing.Optional[int] = None, + label: typing.Optional[str] = None, + resolve: typing.Optional[bool] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> Prompt: + """ + Get a prompt + + Parameters + ---------- + prompt_name : str + The name of the prompt. If the prompt is in a folder (e.g., "folder/subfolder/prompt-name"), + the folder path must be URL encoded. + + version : typing.Optional[int] + Version of the prompt to be retrieved. + + label : typing.Optional[str] + Label of the prompt to be retrieved. Defaults to "production" if no label or version is set. + + resolve : typing.Optional[bool] + Resolve prompt dependencies before returning the prompt. Defaults to `true`. Set to `false` to return the raw stored prompt with dependency tags intact. This bypasses prompt caching and is intended for debugging or one-off jobs, not production runtime fetches. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Prompt + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.prompts.get( + prompt_name="promptName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + prompt_name, + version=version, + label=label, + resolve=resolve, + request_options=request_options, + ) + return _response.data + + async def list( + self, + *, + name: typing.Optional[str] = None, + label: typing.Optional[str] = None, + tag: typing.Optional[str] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_updated_at: typing.Optional[dt.datetime] = None, + to_updated_at: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PromptMetaListResponse: + """ + Get a list of prompt names with versions and labels + + Parameters + ---------- + name : typing.Optional[str] + + label : typing.Optional[str] + + tag : typing.Optional[str] + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + from_updated_at : typing.Optional[dt.datetime] + Optional filter to only include prompt versions created/updated on or after a certain datetime (ISO 8601) + + to_updated_at : typing.Optional[dt.datetime] + Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PromptMetaListResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.prompts.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + name=name, + label=label, + tag=tag, + page=page, + limit=limit, + from_updated_at=from_updated_at, + to_updated_at=to_updated_at, + request_options=request_options, + ) + return _response.data + + async def create( + self, + *, + request: CreatePromptRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> Prompt: + """ + Create a new version for the prompt with the given `name` + + Parameters + ---------- + request : CreatePromptRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Prompt + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.prompts import ( + ChatMessage, + CreateChatPromptRequest, + CreateChatPromptType, + ) + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.prompts.create( + request=CreateChatPromptRequest( + name="name", + prompt=[ + ChatMessage( + role="role", + content="content", + ), + ChatMessage( + role="role", + content="content", + ), + ], + type=CreateChatPromptType.CHAT, + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + request=request, request_options=request_options + ) + return _response.data + + async def delete( + self, + prompt_name: str, + *, + label: typing.Optional[str] = None, + version: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> None: + """ + Delete prompt versions. If neither version nor label is specified, all versions of the prompt are deleted. + + Parameters + ---------- + prompt_name : str + The name of the prompt + + label : typing.Optional[str] + Optional label to filter deletion. If specified, deletes all prompt versions that have this label. + + version : typing.Optional[int] + Optional version to filter deletion. If specified, deletes only this specific version of the prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.prompts.delete( + prompt_name="promptName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete( + prompt_name, label=label, version=version, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/prompts/raw_client.py b/langfuse/api/prompts/raw_client.py new file mode 100644 index 000000000..a2d81a9d1 --- /dev/null +++ b/langfuse/api/prompts/raw_client.py @@ -0,0 +1,987 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from .types.create_prompt_request import CreatePromptRequest +from .types.prompt import Prompt +from .types.prompt_meta_list_response import PromptMetaListResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawPromptsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get( + self, + prompt_name: str, + *, + version: typing.Optional[int] = None, + label: typing.Optional[str] = None, + resolve: typing.Optional[bool] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Prompt]: + """ + Get a prompt + + Parameters + ---------- + prompt_name : str + The name of the prompt. If the prompt is in a folder (e.g., "folder/subfolder/prompt-name"), + the folder path must be URL encoded. + + version : typing.Optional[int] + Version of the prompt to be retrieved. + + label : typing.Optional[str] + Label of the prompt to be retrieved. Defaults to "production" if no label or version is set. + + resolve : typing.Optional[bool] + Resolve prompt dependencies before returning the prompt. Defaults to `true`. Set to `false` to return the raw stored prompt with dependency tags intact. This bypasses prompt caching and is intended for debugging or one-off jobs, not production runtime fetches. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Prompt] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}", + method="GET", + params={ + "version": version, + "label": label, + "resolve": resolve, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Prompt, + parse_obj_as( + type_=Prompt, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def list( + self, + *, + name: typing.Optional[str] = None, + label: typing.Optional[str] = None, + tag: typing.Optional[str] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_updated_at: typing.Optional[dt.datetime] = None, + to_updated_at: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PromptMetaListResponse]: + """ + Get a list of prompt names with versions and labels + + Parameters + ---------- + name : typing.Optional[str] + + label : typing.Optional[str] + + tag : typing.Optional[str] + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + from_updated_at : typing.Optional[dt.datetime] + Optional filter to only include prompt versions created/updated on or after a certain datetime (ISO 8601) + + to_updated_at : typing.Optional[dt.datetime] + Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PromptMetaListResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/v2/prompts", + method="GET", + params={ + "name": name, + "label": label, + "tag": tag, + "page": page, + "limit": limit, + "fromUpdatedAt": serialize_datetime(from_updated_at) + if from_updated_at is not None + else None, + "toUpdatedAt": serialize_datetime(to_updated_at) + if to_updated_at is not None + else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PromptMetaListResponse, + parse_obj_as( + type_=PromptMetaListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create( + self, + *, + request: CreatePromptRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Prompt]: + """ + Create a new version for the prompt with the given `name` + + Parameters + ---------- + request : CreatePromptRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Prompt] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/v2/prompts", + method="POST", + json=convert_and_respect_annotation_metadata( + object_=request, annotation=CreatePromptRequest, direction="write" + ), + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Prompt, + parse_obj_as( + type_=Prompt, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete( + self, + prompt_name: str, + *, + label: typing.Optional[str] = None, + version: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[None]: + """ + Delete prompt versions. If neither version nor label is specified, all versions of the prompt are deleted. + + Parameters + ---------- + prompt_name : str + The name of the prompt + + label : typing.Optional[str] + Optional label to filter deletion. If specified, deletes all prompt versions that have this label. + + version : typing.Optional[int] + Optional version to filter deletion. If specified, deletes only this specific version of the prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}", + method="DELETE", + params={ + "label": label, + "version": version, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawPromptsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get( + self, + prompt_name: str, + *, + version: typing.Optional[int] = None, + label: typing.Optional[str] = None, + resolve: typing.Optional[bool] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Prompt]: + """ + Get a prompt + + Parameters + ---------- + prompt_name : str + The name of the prompt. If the prompt is in a folder (e.g., "folder/subfolder/prompt-name"), + the folder path must be URL encoded. + + version : typing.Optional[int] + Version of the prompt to be retrieved. + + label : typing.Optional[str] + Label of the prompt to be retrieved. Defaults to "production" if no label or version is set. + + resolve : typing.Optional[bool] + Resolve prompt dependencies before returning the prompt. Defaults to `true`. Set to `false` to return the raw stored prompt with dependency tags intact. This bypasses prompt caching and is intended for debugging or one-off jobs, not production runtime fetches. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Prompt] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}", + method="GET", + params={ + "version": version, + "label": label, + "resolve": resolve, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Prompt, + parse_obj_as( + type_=Prompt, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def list( + self, + *, + name: typing.Optional[str] = None, + label: typing.Optional[str] = None, + tag: typing.Optional[str] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_updated_at: typing.Optional[dt.datetime] = None, + to_updated_at: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PromptMetaListResponse]: + """ + Get a list of prompt names with versions and labels + + Parameters + ---------- + name : typing.Optional[str] + + label : typing.Optional[str] + + tag : typing.Optional[str] + + page : typing.Optional[int] + page number, starts at 1 + + limit : typing.Optional[int] + limit of items per page + + from_updated_at : typing.Optional[dt.datetime] + Optional filter to only include prompt versions created/updated on or after a certain datetime (ISO 8601) + + to_updated_at : typing.Optional[dt.datetime] + Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PromptMetaListResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/v2/prompts", + method="GET", + params={ + "name": name, + "label": label, + "tag": tag, + "page": page, + "limit": limit, + "fromUpdatedAt": serialize_datetime(from_updated_at) + if from_updated_at is not None + else None, + "toUpdatedAt": serialize_datetime(to_updated_at) + if to_updated_at is not None + else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PromptMetaListResponse, + parse_obj_as( + type_=PromptMetaListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create( + self, + *, + request: CreatePromptRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Prompt]: + """ + Create a new version for the prompt with the given `name` + + Parameters + ---------- + request : CreatePromptRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Prompt] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/v2/prompts", + method="POST", + json=convert_and_respect_annotation_metadata( + object_=request, annotation=CreatePromptRequest, direction="write" + ), + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Prompt, + parse_obj_as( + type_=Prompt, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete( + self, + prompt_name: str, + *, + label: typing.Optional[str] = None, + version: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[None]: + """ + Delete prompt versions. If neither version nor label is specified, all versions of the prompt are deleted. + + Parameters + ---------- + prompt_name : str + The name of the prompt + + label : typing.Optional[str] + Optional label to filter deletion. If specified, deletes all prompt versions that have this label. + + version : typing.Optional[int] + Optional version to filter deletion. If specified, deletes only this specific version of the prompt. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}", + method="DELETE", + params={ + "label": label, + "version": version, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/prompts/types/__init__.py b/langfuse/api/prompts/types/__init__.py new file mode 100644 index 000000000..91baf935b --- /dev/null +++ b/langfuse/api/prompts/types/__init__.py @@ -0,0 +1,96 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .base_prompt import BasePrompt + from .chat_message import ChatMessage + from .chat_message_type import ChatMessageType + from .chat_message_with_placeholders import ChatMessageWithPlaceholders + from .chat_prompt import ChatPrompt + from .create_chat_prompt_request import CreateChatPromptRequest + from .create_chat_prompt_type import CreateChatPromptType + from .create_prompt_request import CreatePromptRequest + from .create_text_prompt_request import CreateTextPromptRequest + from .create_text_prompt_type import CreateTextPromptType + from .placeholder_message import PlaceholderMessage + from .placeholder_message_type import PlaceholderMessageType + from .prompt import Prompt, Prompt_Chat, Prompt_Text + from .prompt_meta import PromptMeta + from .prompt_meta_list_response import PromptMetaListResponse + from .prompt_type import PromptType + from .text_prompt import TextPrompt +_dynamic_imports: typing.Dict[str, str] = { + "BasePrompt": ".base_prompt", + "ChatMessage": ".chat_message", + "ChatMessageType": ".chat_message_type", + "ChatMessageWithPlaceholders": ".chat_message_with_placeholders", + "ChatPrompt": ".chat_prompt", + "CreateChatPromptRequest": ".create_chat_prompt_request", + "CreateChatPromptType": ".create_chat_prompt_type", + "CreatePromptRequest": ".create_prompt_request", + "CreateTextPromptRequest": ".create_text_prompt_request", + "CreateTextPromptType": ".create_text_prompt_type", + "PlaceholderMessage": ".placeholder_message", + "PlaceholderMessageType": ".placeholder_message_type", + "Prompt": ".prompt", + "PromptMeta": ".prompt_meta", + "PromptMetaListResponse": ".prompt_meta_list_response", + "PromptType": ".prompt_type", + "Prompt_Chat": ".prompt", + "Prompt_Text": ".prompt", + "TextPrompt": ".text_prompt", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BasePrompt", + "ChatMessage", + "ChatMessageType", + "ChatMessageWithPlaceholders", + "ChatPrompt", + "CreateChatPromptRequest", + "CreateChatPromptType", + "CreatePromptRequest", + "CreateTextPromptRequest", + "CreateTextPromptType", + "PlaceholderMessage", + "PlaceholderMessageType", + "Prompt", + "PromptMeta", + "PromptMetaListResponse", + "PromptType", + "Prompt_Chat", + "Prompt_Text", + "TextPrompt", +] diff --git a/langfuse/api/prompts/types/base_prompt.py b/langfuse/api/prompts/types/base_prompt.py new file mode 100644 index 000000000..73cc4a12e --- /dev/null +++ b/langfuse/api/prompts/types/base_prompt.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class BasePrompt(UniversalBaseModel): + name: str + version: int + config: typing.Any + labels: typing.List[str] = pydantic.Field() + """ + List of deployment labels of this prompt version. + """ + + tags: typing.List[str] = pydantic.Field() + """ + List of tags. Used to filter via UI and API. The same across versions of a prompt. + """ + + commit_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="commitMessage") + ] = pydantic.Field(default=None) + """ + Commit message for this prompt version. + """ + + resolution_graph: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, typing.Any]], + FieldMetadata(alias="resolutionGraph"), + ] = pydantic.Field(default=None) + """ + The dependency resolution graph for the current prompt. Null if the prompt has no dependencies or if `resolve=false` was used. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/prompts/types/chat_message.py b/langfuse/api/prompts/types/chat_message.py new file mode 100644 index 000000000..9d0b4c0f6 --- /dev/null +++ b/langfuse/api/prompts/types/chat_message.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .chat_message_type import ChatMessageType + + +class ChatMessage(UniversalBaseModel): + role: str + content: str + type: typing.Optional[ChatMessageType] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/prompts/types/chat_message_type.py b/langfuse/api/prompts/types/chat_message_type.py new file mode 100644 index 000000000..75eac8a20 --- /dev/null +++ b/langfuse/api/prompts/types/chat_message_type.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class ChatMessageType(enum.StrEnum): + CHATMESSAGE = "chatmessage" + + def visit(self, chatmessage: typing.Callable[[], T_Result]) -> T_Result: + if self is ChatMessageType.CHATMESSAGE: + return chatmessage() diff --git a/langfuse/api/prompts/types/chat_message_with_placeholders.py b/langfuse/api/prompts/types/chat_message_with_placeholders.py new file mode 100644 index 000000000..e077ca144 --- /dev/null +++ b/langfuse/api/prompts/types/chat_message_with_placeholders.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .chat_message import ChatMessage +from .placeholder_message import PlaceholderMessage + +ChatMessageWithPlaceholders = typing.Union[ChatMessage, PlaceholderMessage] diff --git a/langfuse/api/prompts/types/chat_prompt.py b/langfuse/api/prompts/types/chat_prompt.py new file mode 100644 index 000000000..ce347537f --- /dev/null +++ b/langfuse/api/prompts/types/chat_prompt.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_prompt import BasePrompt +from .chat_message_with_placeholders import ChatMessageWithPlaceholders + + +class ChatPrompt(BasePrompt): + prompt: typing.List[ChatMessageWithPlaceholders] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/prompts/types/create_chat_prompt_request.py b/langfuse/api/prompts/types/create_chat_prompt_request.py new file mode 100644 index 000000000..0fe1de0c1 --- /dev/null +++ b/langfuse/api/prompts/types/create_chat_prompt_request.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .chat_message_with_placeholders import ChatMessageWithPlaceholders +from .create_chat_prompt_type import CreateChatPromptType + + +class CreateChatPromptRequest(UniversalBaseModel): + name: str + prompt: typing.List[ChatMessageWithPlaceholders] + config: typing.Optional[typing.Any] = None + type: CreateChatPromptType + labels: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of deployment labels of this prompt version. + """ + + tags: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of tags to apply to all versions of this prompt. + """ + + commit_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="commitMessage") + ] = pydantic.Field(default=None) + """ + Commit message for this prompt version. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/prompts/types/create_chat_prompt_type.py b/langfuse/api/prompts/types/create_chat_prompt_type.py new file mode 100644 index 000000000..12f6748a4 --- /dev/null +++ b/langfuse/api/prompts/types/create_chat_prompt_type.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class CreateChatPromptType(enum.StrEnum): + CHAT = "chat" + + def visit(self, chat: typing.Callable[[], T_Result]) -> T_Result: + if self is CreateChatPromptType.CHAT: + return chat() diff --git a/langfuse/api/prompts/types/create_prompt_request.py b/langfuse/api/prompts/types/create_prompt_request.py new file mode 100644 index 000000000..13d75e7e1 --- /dev/null +++ b/langfuse/api/prompts/types/create_prompt_request.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .create_chat_prompt_request import CreateChatPromptRequest +from .create_text_prompt_request import CreateTextPromptRequest + +CreatePromptRequest = typing.Union[CreateChatPromptRequest, CreateTextPromptRequest] diff --git a/langfuse/api/prompts/types/create_text_prompt_request.py b/langfuse/api/prompts/types/create_text_prompt_request.py new file mode 100644 index 000000000..be87a7dde --- /dev/null +++ b/langfuse/api/prompts/types/create_text_prompt_request.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .create_text_prompt_type import CreateTextPromptType + + +class CreateTextPromptRequest(UniversalBaseModel): + name: str + prompt: str + config: typing.Optional[typing.Any] = None + type: typing.Optional[CreateTextPromptType] = None + labels: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of deployment labels of this prompt version. + """ + + tags: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of tags to apply to all versions of this prompt. + """ + + commit_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="commitMessage") + ] = pydantic.Field(default=None) + """ + Commit message for this prompt version. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/prompts/types/create_text_prompt_type.py b/langfuse/api/prompts/types/create_text_prompt_type.py new file mode 100644 index 000000000..825fcee0d --- /dev/null +++ b/langfuse/api/prompts/types/create_text_prompt_type.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class CreateTextPromptType(enum.StrEnum): + TEXT = "text" + + def visit(self, text: typing.Callable[[], T_Result]) -> T_Result: + if self is CreateTextPromptType.TEXT: + return text() diff --git a/langfuse/api/prompts/types/placeholder_message.py b/langfuse/api/prompts/types/placeholder_message.py new file mode 100644 index 000000000..9397e20d0 --- /dev/null +++ b/langfuse/api/prompts/types/placeholder_message.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .placeholder_message_type import PlaceholderMessageType + + +class PlaceholderMessage(UniversalBaseModel): + name: str + type: typing.Optional[PlaceholderMessageType] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/prompts/types/placeholder_message_type.py b/langfuse/api/prompts/types/placeholder_message_type.py new file mode 100644 index 000000000..511e98c87 --- /dev/null +++ b/langfuse/api/prompts/types/placeholder_message_type.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class PlaceholderMessageType(enum.StrEnum): + PLACEHOLDER = "placeholder" + + def visit(self, placeholder: typing.Callable[[], T_Result]) -> T_Result: + if self is PlaceholderMessageType.PLACEHOLDER: + return placeholder() diff --git a/langfuse/api/prompts/types/prompt.py b/langfuse/api/prompts/types/prompt.py new file mode 100644 index 000000000..813e5992f --- /dev/null +++ b/langfuse/api/prompts/types/prompt.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .chat_message_with_placeholders import ChatMessageWithPlaceholders + + +class Prompt_Chat(UniversalBaseModel): + type: typing.Literal["chat"] = "chat" + prompt: typing.List[ChatMessageWithPlaceholders] + name: str + version: int + config: typing.Any + labels: typing.List[str] + tags: typing.List[str] + commit_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="commitMessage") + ] = None + resolution_graph: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, typing.Any]], + FieldMetadata(alias="resolutionGraph"), + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class Prompt_Text(UniversalBaseModel): + type: typing.Literal["text"] = "text" + prompt: str + name: str + version: int + config: typing.Any + labels: typing.List[str] + tags: typing.List[str] + commit_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="commitMessage") + ] = None + resolution_graph: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, typing.Any]], + FieldMetadata(alias="resolutionGraph"), + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +Prompt = typing_extensions.Annotated[ + typing.Union[Prompt_Chat, Prompt_Text], pydantic.Field(discriminator="type") +] diff --git a/langfuse/api/prompts/types/prompt_meta.py b/langfuse/api/prompts/types/prompt_meta.py new file mode 100644 index 000000000..974a50252 --- /dev/null +++ b/langfuse/api/prompts/types/prompt_meta.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .prompt_type import PromptType + + +class PromptMeta(UniversalBaseModel): + name: str + type: PromptType = pydantic.Field() + """ + Indicates whether the prompt is a text or chat prompt. + """ + + versions: typing.List[int] + labels: typing.List[str] + tags: typing.List[str] + last_updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="lastUpdatedAt") + ] + last_config: typing_extensions.Annotated[ + typing.Any, FieldMetadata(alias="lastConfig") + ] = pydantic.Field() + """ + Config object of the most recent prompt version that matches the filters (if any are provided) + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/prompts/types/prompt_meta_list_response.py b/langfuse/api/prompts/types/prompt_meta_list_response.py new file mode 100644 index 000000000..22043d008 --- /dev/null +++ b/langfuse/api/prompts/types/prompt_meta_list_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse +from .prompt_meta import PromptMeta + + +class PromptMetaListResponse(UniversalBaseModel): + data: typing.List[PromptMeta] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/prompts/types/prompt_type.py b/langfuse/api/prompts/types/prompt_type.py new file mode 100644 index 000000000..7d8230db2 --- /dev/null +++ b/langfuse/api/prompts/types/prompt_type.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core import enum + +T_Result = typing.TypeVar("T_Result") + + +class PromptType(enum.StrEnum): + CHAT = "chat" + TEXT = "text" + + def visit( + self, chat: typing.Callable[[], T_Result], text: typing.Callable[[], T_Result] + ) -> T_Result: + if self is PromptType.CHAT: + return chat() + if self is PromptType.TEXT: + return text() diff --git a/langfuse/api/prompts/types/text_prompt.py b/langfuse/api/prompts/types/text_prompt.py new file mode 100644 index 000000000..fbc53c2f5 --- /dev/null +++ b/langfuse/api/prompts/types/text_prompt.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_prompt import BasePrompt + + +class TextPrompt(BasePrompt): + prompt: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/reference.md b/langfuse/api/reference.md deleted file mode 100644 index 29a7db88b..000000000 --- a/langfuse/api/reference.md +++ /dev/null @@ -1,6080 +0,0 @@ -# Reference -## AnnotationQueues -
client.annotation_queues.list_queues(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get all annotation queues -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.annotation_queues.list_queues() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — limit of items per page - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.annotation_queues.get_queue(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get an annotation queue by ID -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.annotation_queues.get_queue( - queue_id="queueId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**queue_id:** `str` — The unique identifier of the annotation queue - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.annotation_queues.list_queue_items(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get items for a specific annotation queue -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.annotation_queues.list_queue_items( - queue_id="queueId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**queue_id:** `str` — The unique identifier of the annotation queue - -
-
- -
-
- -**status:** `typing.Optional[AnnotationQueueStatus]` — Filter by status - -
-
- -
-
- -**page:** `typing.Optional[int]` — page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — limit of items per page - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.annotation_queues.get_queue_item(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a specific item from an annotation queue -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.annotation_queues.get_queue_item( - queue_id="queueId", - item_id="itemId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**queue_id:** `str` — The unique identifier of the annotation queue - -
-
- -
-
- -**item_id:** `str` — The unique identifier of the annotation queue item - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.annotation_queues.create_queue_item(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Add an item to an annotation queue -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import AnnotationQueueObjectType, CreateAnnotationQueueItemRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.annotation_queues.create_queue_item( - queue_id="queueId", - request=CreateAnnotationQueueItemRequest( - object_id="objectId", - object_type=AnnotationQueueObjectType.TRACE, - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**queue_id:** `str` — The unique identifier of the annotation queue - -
-
- -
-
- -**request:** `CreateAnnotationQueueItemRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.annotation_queues.update_queue_item(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Update an annotation queue item -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import UpdateAnnotationQueueItemRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.annotation_queues.update_queue_item( - queue_id="queueId", - item_id="itemId", - request=UpdateAnnotationQueueItemRequest(), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**queue_id:** `str` — The unique identifier of the annotation queue - -
-
- -
-
- -**item_id:** `str` — The unique identifier of the annotation queue item - -
-
- -
-
- -**request:** `UpdateAnnotationQueueItemRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.annotation_queues.delete_queue_item(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Remove an item from an annotation queue -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.annotation_queues.delete_queue_item( - queue_id="queueId", - item_id="itemId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**queue_id:** `str` — The unique identifier of the annotation queue - -
-
- -
-
- -**item_id:** `str` — The unique identifier of the annotation queue item - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Comments -
client.comments.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a comment. Comments may be attached to different object types (trace, observation, session, prompt). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import CreateCommentRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.comments.create( - request=CreateCommentRequest( - project_id="projectId", - object_type="objectType", - object_id="objectId", - content="content", - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `CreateCommentRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.comments.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get all comments -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.comments.get() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — Page number, starts at 1. - -
-
- -
-
- -**limit:** `typing.Optional[int]` — Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit - -
-
- -
-
- -**object_type:** `typing.Optional[str]` — Filter comments by object type (trace, observation, session, prompt). - -
-
- -
-
- -**object_id:** `typing.Optional[str]` — Filter comments by object id. If objectType is not provided, an error will be thrown. - -
-
- -
-
- -**author_user_id:** `typing.Optional[str]` — Filter comments by author user id. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.comments.get_by_id(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a comment by id -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.comments.get_by_id( - comment_id="commentId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**comment_id:** `str` — The unique langfuse identifier of a comment - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## DatasetItems -
client.dataset_items.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a dataset item -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import CreateDatasetItemRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.dataset_items.create( - request=CreateDatasetItemRequest( - dataset_name="datasetName", - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `CreateDatasetItemRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dataset_items.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a dataset item -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.dataset_items.get( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dataset_items.list(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get dataset items -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.dataset_items.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**dataset_name:** `typing.Optional[str]` - -
-
- -
-
- -**source_trace_id:** `typing.Optional[str]` - -
-
- -
-
- -**source_observation_id:** `typing.Optional[str]` - -
-
- -
-
- -**page:** `typing.Optional[int]` — page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — limit of items per page - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dataset_items.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a dataset item and all its run items. This action is irreversible. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.dataset_items.delete( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## DatasetRunItems -
client.dataset_run_items.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a dataset run item -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import CreateDatasetRunItemRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.dataset_run_items.create( - request=CreateDatasetRunItemRequest( - run_name="runName", - dataset_item_id="datasetItemId", - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `CreateDatasetRunItemRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dataset_run_items.list(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List dataset run items -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.dataset_run_items.list( - dataset_id="datasetId", - run_name="runName", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**dataset_id:** `str` - -
-
- -
-
- -**run_name:** `str` - -
-
- -
-
- -**page:** `typing.Optional[int]` — page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — limit of items per page - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Datasets -
client.datasets.list(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get all datasets -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.datasets.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — limit of items per page - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.datasets.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a dataset -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.datasets.get( - dataset_name="datasetName", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**dataset_name:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.datasets.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a dataset -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import CreateDatasetRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.datasets.create( - request=CreateDatasetRequest( - name="name", - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `CreateDatasetRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.datasets.get_run(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a dataset run and its items -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.datasets.get_run( - dataset_name="datasetName", - run_name="runName", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**dataset_name:** `str` - -
-
- -
-
- -**run_name:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.datasets.delete_run(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a dataset run and all its run items. This action is irreversible. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.datasets.delete_run( - dataset_name="datasetName", - run_name="runName", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**dataset_name:** `str` - -
-
- -
-
- -**run_name:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.datasets.get_runs(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get dataset runs -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.datasets.get_runs( - dataset_name="datasetName", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**dataset_name:** `str` - -
-
- -
-
- -**page:** `typing.Optional[int]` — page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — limit of items per page - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Health -
client.health.health() -
-
- -#### 📝 Description - -
-
- -
-
- -Check health of API and database -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.health.health() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Ingestion -
client.ingestion.batch(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Batched ingestion for Langfuse Tracing. -If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement. - -Within each batch, there can be multiple events. -Each event has a type, an id, a timestamp, metadata and a body. -Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace. -We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request. -The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App. -I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. - -Notes: -- Introduction to data model: https://langfuse.com/docs/tracing-data-model -- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. -- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import IngestionEvent_ScoreCreate, ScoreBody -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.ingestion.batch( - batch=[ - IngestionEvent_ScoreCreate( - id="abcdef-1234-5678-90ab", - timestamp="2022-01-01T00:00:00.000Z", - body=ScoreBody( - id="abcdef-1234-5678-90ab", - trace_id="1234-5678-90ab-cdef", - name="My Score", - value=0.9, - environment="default", - ), - ) - ], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**batch:** `typing.Sequence[IngestionEvent]` — Batch of tracing events to be ingested. Discriminated by attribute `type`. - -
-
- -
-
- -**metadata:** `typing.Optional[typing.Any]` — Optional. Metadata field used by the Langfuse SDKs for debugging. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Media -
client.media.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a media record -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.media.get( - media_id="mediaId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**media_id:** `str` — The unique langfuse identifier of a media record - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.media.patch(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Patch a media record -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -import datetime - -from langfuse import PatchMediaBody -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.media.patch( - media_id="mediaId", - request=PatchMediaBody( - uploaded_at=datetime.datetime.fromisoformat( - "2024-01-15 09:30:00+00:00", - ), - upload_http_status=1, - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**media_id:** `str` — The unique langfuse identifier of a media record - -
-
- -
-
- -**request:** `PatchMediaBody` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.media.get_upload_url(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a presigned upload URL for a media record -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import GetMediaUploadUrlRequest, MediaContentType -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.media.get_upload_url( - request=GetMediaUploadUrlRequest( - trace_id="traceId", - content_type=MediaContentType.IMAGE_PNG, - content_length=1, - sha_256_hash="sha256Hash", - field="field", - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `GetMediaUploadUrlRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Metrics -
client.metrics.metrics(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get metrics from the Langfuse project using a query object -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.metrics.metrics( - query="query", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**query:** `str` - -JSON string containing the query parameters with the following structure: -```json -{ - "view": string, // Required. One of "traces", "observations", "scores-numeric", "scores-categorical" - "dimensions": [ // Optional. Default: [] - { - "field": string // Field to group by, e.g. "name", "userId", "sessionId" - } - ], - "metrics": [ // Required. At least one metric must be provided - { - "measure": string, // What to measure, e.g. "count", "latency", "value" - "aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram" - } - ], - "filters": [ // Optional. Default: [] - { - "column": string, // Column to filter on - "operator": string, // Operator, e.g. "=", ">", "<", "contains" - "value": any, // Value to compare against - "type": string, // Data type, e.g. "string", "number", "stringObject" - "key": string // Required only when filtering on metadata - } - ], - "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time - "granularity": string // One of "minute", "hour", "day", "week", "month", "auto" - }, - "fromTimestamp": string, // Required. ISO datetime string for start of time range - "toTimestamp": string, // Required. ISO datetime string for end of time range - "orderBy": [ // Optional. Default: null - { - "field": string, // Field to order by - "direction": string // "asc" or "desc" - } - ], - "config": { // Optional. Query-specific configuration - "bins": number, // Optional. Number of bins for histogram (1-100), default: 10 - "row_limit": number // Optional. Row limit for results (1-1000) - } -} -``` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Models -
client.models.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a model -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import CreateModelRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.models.create( - request=CreateModelRequest( - model_name="modelName", - match_pattern="matchPattern", - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `CreateModelRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.models.list(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get all models -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.models.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — limit of items per page - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.models.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a model -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.models.get( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.models.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.models.delete( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Observations -
client.observations.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a observation -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.observations.get( - observation_id="observationId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**observation_id:** `str` — The unique langfuse identifier of an observation, can be an event, span or generation - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.observations.get_many(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a list of observations -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.observations.get_many() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — Page number, starts at 1. - -
-
- -
-
- -**limit:** `typing.Optional[int]` — Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**user_id:** `typing.Optional[str]` - -
-
- -
-
- -**type:** `typing.Optional[str]` - -
-
- -
-
- -**trace_id:** `typing.Optional[str]` - -
-
- -
-
- -**parent_observation_id:** `typing.Optional[str]` - -
-
- -
-
- -**environment:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — Optional filter for observations where the environment is one of the provided values. - -
-
- -
-
- -**from_start_time:** `typing.Optional[dt.datetime]` — Retrieve only observations with a start_time on or after this datetime (ISO 8601). - -
-
- -
-
- -**to_start_time:** `typing.Optional[dt.datetime]` — Retrieve only observations with a start_time before this datetime (ISO 8601). - -
-
- -
-
- -**version:** `typing.Optional[str]` — Optional filter to only include observations with a certain version. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Organizations -
client.organizations.get_organization_memberships() -
-
- -#### 📝 Description - -
-
- -
-
- -Get all memberships for the organization associated with the API key (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.organizations.get_organization_memberships() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.organizations.update_organization_membership(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create or update a membership for the organization associated with the API key (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import MembershipRequest, MembershipRole -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.organizations.update_organization_membership( - request=MembershipRequest( - user_id="userId", - role=MembershipRole.OWNER, - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `MembershipRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.organizations.get_project_memberships(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get all memberships for a specific project (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.organizations.get_project_memberships( - project_id="projectId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**project_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.organizations.update_project_membership(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create or update a membership for a specific project (requires organization-scoped API key). The user must already be a member of the organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import MembershipRequest, MembershipRole -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.organizations.update_project_membership( - project_id="projectId", - request=MembershipRequest( - user_id="userId", - role=MembershipRole.OWNER, - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**project_id:** `str` - -
-
- -
-
- -**request:** `MembershipRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.organizations.get_organization_projects() -
-
- -#### 📝 Description - -
-
- -
-
- -Get all projects for the organization associated with the API key (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.organizations.get_organization_projects() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Projects -
client.projects.get() -
-
- -#### 📝 Description - -
-
- -
-
- -Get Project associated with API key -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.projects.get() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.projects.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new project (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.projects.create( - name="name", - retention=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` - -
-
- -
-
- -**retention:** `int` — Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. - -
-
- -
-
- -**metadata:** `typing.Optional[typing.Dict[str, typing.Any]]` — Optional metadata for the project - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.projects.update(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Update a project by ID (requires organization-scoped API key). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.projects.update( - project_id="projectId", - name="name", - retention=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**project_id:** `str` - -
-
- -
-
- -**name:** `str` - -
-
- -
-
- -**retention:** `int` — Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. - -
-
- -
-
- -**metadata:** `typing.Optional[typing.Dict[str, typing.Any]]` — Optional metadata for the project - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.projects.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.projects.delete( - project_id="projectId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**project_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.projects.get_api_keys(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get all API keys for a project (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.projects.get_api_keys( - project_id="projectId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**project_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.projects.create_api_key(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new API key for a project (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.projects.create_api_key( - project_id="projectId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**project_id:** `str` - -
-
- -
-
- -**note:** `typing.Optional[str]` — Optional note for the API key - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.projects.delete_api_key(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete an API key for a project (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.projects.delete_api_key( - project_id="projectId", - api_key_id="apiKeyId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**project_id:** `str` - -
-
- -
-
- -**api_key_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## PromptVersion -
client.prompt_version.update(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Update labels for a specific prompt version -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.prompt_version.update( - name="name", - version=1, - new_labels=["newLabels", "newLabels"], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` — The name of the prompt - -
-
- -
-
- -**version:** `int` — Version of the prompt to update - -
-
- -
-
- -**new_labels:** `typing.Sequence[str]` — New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Prompts -
client.prompts.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a prompt -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.prompts.get( - prompt_name="promptName", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**prompt_name:** `str` — The name of the prompt - -
-
- -
-
- -**version:** `typing.Optional[int]` — Version of the prompt to be retrieved. - -
-
- -
-
- -**label:** `typing.Optional[str]` — Label of the prompt to be retrieved. Defaults to "production" if no label or version is set. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.prompts.list(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a list of prompt names with versions and labels -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.prompts.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**label:** `typing.Optional[str]` - -
-
- -
-
- -**tag:** `typing.Optional[str]` - -
-
- -
-
- -**page:** `typing.Optional[int]` — page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — limit of items per page - -
-
- -
-
- -**from_updated_at:** `typing.Optional[dt.datetime]` — Optional filter to only include prompt versions created/updated on or after a certain datetime (ISO 8601) - -
-
- -
-
- -**to_updated_at:** `typing.Optional[dt.datetime]` — Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.prompts.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new version for the prompt with the given `name` -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import ( - ChatMessageWithPlaceholders_Chatmessage, - CreatePromptRequest_Chat, -) -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.prompts.create( - request=CreatePromptRequest_Chat( - name="name", - prompt=[ - ChatMessageWithPlaceholders_Chatmessage( - role="role", - content="content", - ), - ChatMessageWithPlaceholders_Chatmessage( - role="role", - content="content", - ), - ], - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `CreatePromptRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Scim -
client.scim.get_service_provider_config() -
-
- -#### 📝 Description - -
-
- -
-
- -Get SCIM Service Provider Configuration (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.scim.get_service_provider_config() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.scim.get_resource_types() -
-
- -#### 📝 Description - -
-
- -
-
- -Get SCIM Resource Types (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.scim.get_resource_types() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.scim.get_schemas() -
-
- -#### 📝 Description - -
-
- -
-
- -Get SCIM Schemas (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.scim.get_schemas() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.scim.list_users(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List users in the organization (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.scim.list_users() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**filter:** `typing.Optional[str]` — Filter expression (e.g. userName eq "value") - -
-
- -
-
- -**start_index:** `typing.Optional[int]` — 1-based index of the first result to return (default 1) - -
-
- -
-
- -**count:** `typing.Optional[int]` — Maximum number of results to return (default 100) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.scim.create_user(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new user in the organization (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import ScimName -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.scim.create_user( - user_name="userName", - name=ScimName(), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**user_name:** `str` — User's email address (required) - -
-
- -
-
- -**name:** `ScimName` — User's name information - -
-
- -
-
- -**emails:** `typing.Optional[typing.Sequence[ScimEmail]]` — User's email addresses - -
-
- -
-
- -**active:** `typing.Optional[bool]` — Whether the user is active - -
-
- -
-
- -**password:** `typing.Optional[str]` — Initial password for the user - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.scim.get_user(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a specific user by ID (requires organization-scoped API key) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.scim.get_user( - user_id="userId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**user_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.scim.delete_user(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Remove a user from the organization (requires organization-scoped API key). Note that this only removes the user from the organization but does not delete the user entity itself. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.scim.delete_user( - user_id="userId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**user_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## ScoreConfigs -
client.score_configs.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a score configuration (config). Score configs are used to define the structure of scores -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import CreateScoreConfigRequest, ScoreDataType -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.score_configs.create( - request=CreateScoreConfigRequest( - name="name", - data_type=ScoreDataType.NUMERIC, - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `CreateScoreConfigRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.score_configs.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get all score configs -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.score_configs.get() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — Page number, starts at 1. - -
-
- -
-
- -**limit:** `typing.Optional[int]` — Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.score_configs.get_by_id(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a score config -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.score_configs.get_by_id( - config_id="configId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**config_id:** `str` — The unique langfuse identifier of a score config - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## ScoreV2 -
client.score_v_2.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a list of scores (supports both trace and session scores) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.score_v_2.get() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — Page number, starts at 1. - -
-
- -
-
- -**limit:** `typing.Optional[int]` — Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - -
-
- -
-
- -**user_id:** `typing.Optional[str]` — Retrieve only scores with this userId associated to the trace. - -
-
- -
-
- -**name:** `typing.Optional[str]` — Retrieve only scores with this name. - -
-
- -
-
- -**from_timestamp:** `typing.Optional[dt.datetime]` — Optional filter to only include scores created on or after a certain datetime (ISO 8601) - -
-
- -
-
- -**to_timestamp:** `typing.Optional[dt.datetime]` — Optional filter to only include scores created before a certain datetime (ISO 8601) - -
-
- -
-
- -**environment:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — Optional filter for scores where the environment is one of the provided values. - -
-
- -
-
- -**source:** `typing.Optional[ScoreSource]` — Retrieve only scores from a specific source. - -
-
- -
-
- -**operator:** `typing.Optional[str]` — Retrieve only scores with value. - -
-
- -
-
- -**value:** `typing.Optional[float]` — Retrieve only scores with value. - -
-
- -
-
- -**score_ids:** `typing.Optional[str]` — Comma-separated list of score IDs to limit the results to. - -
-
- -
-
- -**config_id:** `typing.Optional[str]` — Retrieve only scores with a specific configId. - -
-
- -
-
- -**queue_id:** `typing.Optional[str]` — Retrieve only scores with a specific annotation queueId. - -
-
- -
-
- -**data_type:** `typing.Optional[ScoreDataType]` — Retrieve only scores with a specific dataType. - -
-
- -
-
- -**trace_tags:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — Only scores linked to traces that include all of these tags will be returned. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.score_v_2.get_by_id(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a score (supports both trace and session scores) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.score_v_2.get_by_id( - score_id="scoreId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**score_id:** `str` — The unique langfuse identifier of a score - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Score -
client.score.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a score (supports both trace and session scores) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse import CreateScoreRequest -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.score.create( - request=CreateScoreRequest( - name="name", - value=1.1, - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `CreateScoreRequest` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.score.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a score (supports both trace and session scores) -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.score.delete( - score_id="scoreId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**score_id:** `str` — The unique langfuse identifier of a score - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Sessions -
client.sessions.list(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get sessions -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.sessions.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — Page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - -
-
- -
-
- -**from_timestamp:** `typing.Optional[dt.datetime]` — Optional filter to only include sessions created on or after a certain datetime (ISO 8601) - -
-
- -
-
- -**to_timestamp:** `typing.Optional[dt.datetime]` — Optional filter to only include sessions created before a certain datetime (ISO 8601) - -
-
- -
-
- -**environment:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — Optional filter for sessions where the environment is one of the provided values. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.sessions.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=` -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.sessions.get( - session_id="sessionId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**session_id:** `str` — The unique id of a session - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Trace -
client.trace.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get a specific trace -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.trace.get( - trace_id="traceId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**trace_id:** `str` — The unique langfuse identifier of a trace - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.trace.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a specific trace -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.trace.delete( - trace_id="traceId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**trace_id:** `str` — The unique langfuse identifier of the trace to delete - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.trace.list(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Get list of traces -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.trace.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**page:** `typing.Optional[int]` — Page number, starts at 1 - -
-
- -
-
- -**limit:** `typing.Optional[int]` — Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - -
-
- -
-
- -**user_id:** `typing.Optional[str]` - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**session_id:** `typing.Optional[str]` - -
-
- -
-
- -**from_timestamp:** `typing.Optional[dt.datetime]` — Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) - -
-
- -
-
- -**to_timestamp:** `typing.Optional[dt.datetime]` — Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) - -
-
- -
-
- -**order_by:** `typing.Optional[str]` — Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc - -
-
- -
-
- -**tags:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — Only traces that include all of these tags will be returned. - -
-
- -
-
- -**version:** `typing.Optional[str]` — Optional filter to only include traces with a certain version. - -
-
- -
-
- -**release:** `typing.Optional[str]` — Optional filter to only include traces with a certain release. - -
-
- -
-
- -**environment:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — Optional filter for traces where the environment is one of the provided values. - -
-
- -
-
- -**fields:** `typing.Optional[str]` — Comma-separated list of fields to include in the response. Available field groups are 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not provided, all fields are included. Example: 'core,scores,metrics' - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.trace.delete_multiple(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete multiple traces -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from langfuse.client import FernLangfuse - -client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", -) -client.trace.delete_multiple( - trace_ids=["traceIds", "traceIds"], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**trace_ids:** `typing.Sequence[str]` — List of trace IDs to delete - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- diff --git a/langfuse/api/resources/__init__.py b/langfuse/api/resources/__init__.py deleted file mode 100644 index 453774283..000000000 --- a/langfuse/api/resources/__init__.py +++ /dev/null @@ -1,439 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from . import ( - annotation_queues, - comments, - commons, - dataset_items, - dataset_run_items, - datasets, - health, - ingestion, - media, - metrics, - models, - observations, - organizations, - projects, - prompt_version, - prompts, - scim, - score, - score_configs, - score_v_2, - sessions, - trace, - utils, -) -from .annotation_queues import ( - AnnotationQueue, - AnnotationQueueItem, - AnnotationQueueObjectType, - AnnotationQueueStatus, - CreateAnnotationQueueItemRequest, - DeleteAnnotationQueueItemResponse, - PaginatedAnnotationQueueItems, - PaginatedAnnotationQueues, - UpdateAnnotationQueueItemRequest, -) -from .comments import CreateCommentRequest, CreateCommentResponse, GetCommentsResponse -from .commons import ( - AccessDeniedError, - BaseScore, - BaseScoreV1, - BooleanScore, - BooleanScoreV1, - CategoricalScore, - CategoricalScoreV1, - Comment, - CommentObjectType, - ConfigCategory, - CreateScoreValue, - Dataset, - DatasetItem, - DatasetRun, - DatasetRunItem, - DatasetRunWithItems, - DatasetStatus, - Error, - MapValue, - MethodNotAllowedError, - Model, - ModelPrice, - ModelUsageUnit, - NotFoundError, - NumericScore, - NumericScoreV1, - Observation, - ObservationLevel, - ObservationsView, - Score, - ScoreConfig, - ScoreDataType, - ScoreSource, - ScoreV1, - ScoreV1_Boolean, - ScoreV1_Categorical, - ScoreV1_Numeric, - Score_Boolean, - Score_Categorical, - Score_Numeric, - Session, - SessionWithTraces, - Trace, - TraceWithDetails, - TraceWithFullDetails, - UnauthorizedError, - Usage, -) -from .dataset_items import ( - CreateDatasetItemRequest, - DeleteDatasetItemResponse, - PaginatedDatasetItems, -) -from .dataset_run_items import CreateDatasetRunItemRequest, PaginatedDatasetRunItems -from .datasets import ( - CreateDatasetRequest, - DeleteDatasetRunResponse, - PaginatedDatasetRuns, - PaginatedDatasets, -) -from .health import HealthResponse, ServiceUnavailableError -from .ingestion import ( - BaseEvent, - CreateEventBody, - CreateEventEvent, - CreateGenerationBody, - CreateGenerationEvent, - CreateObservationEvent, - CreateSpanBody, - CreateSpanEvent, - IngestionError, - IngestionEvent, - IngestionEvent_EventCreate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, - IngestionEvent_ScoreCreate, - IngestionEvent_SdkLog, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_TraceCreate, - IngestionResponse, - IngestionSuccess, - IngestionUsage, - ObservationBody, - ObservationType, - OpenAiCompletionUsageSchema, - OpenAiResponseUsageSchema, - OpenAiUsage, - OptionalObservationBody, - ScoreBody, - ScoreEvent, - SdkLogBody, - SdkLogEvent, - TraceBody, - TraceEvent, - UpdateEventBody, - UpdateGenerationBody, - UpdateGenerationEvent, - UpdateObservationEvent, - UpdateSpanBody, - UpdateSpanEvent, - UsageDetails, -) -from .media import ( - GetMediaResponse, - GetMediaUploadUrlRequest, - GetMediaUploadUrlResponse, - MediaContentType, - PatchMediaBody, -) -from .metrics import MetricsResponse -from .models import CreateModelRequest, PaginatedModels -from .observations import Observations, ObservationsViews -from .organizations import ( - MembershipRequest, - MembershipResponse, - MembershipRole, - MembershipsResponse, - OrganizationProject, - OrganizationProjectsResponse, -) -from .projects import ( - ApiKeyDeletionResponse, - ApiKeyList, - ApiKeyResponse, - ApiKeySummary, - Project, - ProjectDeletionResponse, - Projects, -) -from .prompts import ( - BasePrompt, - ChatMessage, - ChatMessageWithPlaceholders, - ChatMessageWithPlaceholders_Chatmessage, - ChatMessageWithPlaceholders_Placeholder, - ChatPrompt, - CreateChatPromptRequest, - CreatePromptRequest, - CreatePromptRequest_Chat, - CreatePromptRequest_Text, - CreateTextPromptRequest, - PlaceholderMessage, - Prompt, - PromptMeta, - PromptMetaListResponse, - Prompt_Chat, - Prompt_Text, - TextPrompt, -) -from .scim import ( - AuthenticationScheme, - BulkConfig, - EmptyResponse, - FilterConfig, - ResourceMeta, - ResourceType, - ResourceTypesResponse, - SchemaExtension, - SchemaResource, - SchemasResponse, - ScimEmail, - ScimFeatureSupport, - ScimName, - ScimUser, - ScimUsersListResponse, - ServiceProviderConfig, - UserMeta, -) -from .score import CreateScoreRequest, CreateScoreResponse -from .score_configs import CreateScoreConfigRequest, ScoreConfigs -from .score_v_2 import ( - GetScoresResponse, - GetScoresResponseData, - GetScoresResponseDataBoolean, - GetScoresResponseDataCategorical, - GetScoresResponseDataNumeric, - GetScoresResponseData_Boolean, - GetScoresResponseData_Categorical, - GetScoresResponseData_Numeric, - GetScoresResponseTraceData, -) -from .sessions import PaginatedSessions -from .trace import DeleteTraceResponse, Sort, Traces - -__all__ = [ - "AccessDeniedError", - "AnnotationQueue", - "AnnotationQueueItem", - "AnnotationQueueObjectType", - "AnnotationQueueStatus", - "ApiKeyDeletionResponse", - "ApiKeyList", - "ApiKeyResponse", - "ApiKeySummary", - "AuthenticationScheme", - "BaseEvent", - "BasePrompt", - "BaseScore", - "BaseScoreV1", - "BooleanScore", - "BooleanScoreV1", - "BulkConfig", - "CategoricalScore", - "CategoricalScoreV1", - "ChatMessage", - "ChatMessageWithPlaceholders", - "ChatMessageWithPlaceholders_Chatmessage", - "ChatMessageWithPlaceholders_Placeholder", - "ChatPrompt", - "Comment", - "CommentObjectType", - "ConfigCategory", - "CreateAnnotationQueueItemRequest", - "CreateChatPromptRequest", - "CreateCommentRequest", - "CreateCommentResponse", - "CreateDatasetItemRequest", - "CreateDatasetRequest", - "CreateDatasetRunItemRequest", - "CreateEventBody", - "CreateEventEvent", - "CreateGenerationBody", - "CreateGenerationEvent", - "CreateModelRequest", - "CreateObservationEvent", - "CreatePromptRequest", - "CreatePromptRequest_Chat", - "CreatePromptRequest_Text", - "CreateScoreConfigRequest", - "CreateScoreRequest", - "CreateScoreResponse", - "CreateScoreValue", - "CreateSpanBody", - "CreateSpanEvent", - "CreateTextPromptRequest", - "Dataset", - "DatasetItem", - "DatasetRun", - "DatasetRunItem", - "DatasetRunWithItems", - "DatasetStatus", - "DeleteAnnotationQueueItemResponse", - "DeleteDatasetItemResponse", - "DeleteDatasetRunResponse", - "DeleteTraceResponse", - "EmptyResponse", - "Error", - "FilterConfig", - "GetCommentsResponse", - "GetMediaResponse", - "GetMediaUploadUrlRequest", - "GetMediaUploadUrlResponse", - "GetScoresResponse", - "GetScoresResponseData", - "GetScoresResponseDataBoolean", - "GetScoresResponseDataCategorical", - "GetScoresResponseDataNumeric", - "GetScoresResponseData_Boolean", - "GetScoresResponseData_Categorical", - "GetScoresResponseData_Numeric", - "GetScoresResponseTraceData", - "HealthResponse", - "IngestionError", - "IngestionEvent", - "IngestionEvent_EventCreate", - "IngestionEvent_GenerationCreate", - "IngestionEvent_GenerationUpdate", - "IngestionEvent_ObservationCreate", - "IngestionEvent_ObservationUpdate", - "IngestionEvent_ScoreCreate", - "IngestionEvent_SdkLog", - "IngestionEvent_SpanCreate", - "IngestionEvent_SpanUpdate", - "IngestionEvent_TraceCreate", - "IngestionResponse", - "IngestionSuccess", - "IngestionUsage", - "MapValue", - "MediaContentType", - "MembershipRequest", - "MembershipResponse", - "MembershipRole", - "MembershipsResponse", - "MethodNotAllowedError", - "MetricsResponse", - "Model", - "ModelPrice", - "ModelUsageUnit", - "NotFoundError", - "NumericScore", - "NumericScoreV1", - "Observation", - "ObservationBody", - "ObservationLevel", - "ObservationType", - "Observations", - "ObservationsView", - "ObservationsViews", - "OpenAiCompletionUsageSchema", - "OpenAiResponseUsageSchema", - "OpenAiUsage", - "OptionalObservationBody", - "OrganizationProject", - "OrganizationProjectsResponse", - "PaginatedAnnotationQueueItems", - "PaginatedAnnotationQueues", - "PaginatedDatasetItems", - "PaginatedDatasetRunItems", - "PaginatedDatasetRuns", - "PaginatedDatasets", - "PaginatedModels", - "PaginatedSessions", - "PatchMediaBody", - "PlaceholderMessage", - "Project", - "ProjectDeletionResponse", - "Projects", - "Prompt", - "PromptMeta", - "PromptMetaListResponse", - "Prompt_Chat", - "Prompt_Text", - "ResourceMeta", - "ResourceType", - "ResourceTypesResponse", - "SchemaExtension", - "SchemaResource", - "SchemasResponse", - "ScimEmail", - "ScimFeatureSupport", - "ScimName", - "ScimUser", - "ScimUsersListResponse", - "Score", - "ScoreBody", - "ScoreConfig", - "ScoreConfigs", - "ScoreDataType", - "ScoreEvent", - "ScoreSource", - "ScoreV1", - "ScoreV1_Boolean", - "ScoreV1_Categorical", - "ScoreV1_Numeric", - "Score_Boolean", - "Score_Categorical", - "Score_Numeric", - "SdkLogBody", - "SdkLogEvent", - "ServiceProviderConfig", - "ServiceUnavailableError", - "Session", - "SessionWithTraces", - "Sort", - "TextPrompt", - "Trace", - "TraceBody", - "TraceEvent", - "TraceWithDetails", - "TraceWithFullDetails", - "Traces", - "UnauthorizedError", - "UpdateAnnotationQueueItemRequest", - "UpdateEventBody", - "UpdateGenerationBody", - "UpdateGenerationEvent", - "UpdateObservationEvent", - "UpdateSpanBody", - "UpdateSpanEvent", - "Usage", - "UsageDetails", - "UserMeta", - "annotation_queues", - "comments", - "commons", - "dataset_items", - "dataset_run_items", - "datasets", - "health", - "ingestion", - "media", - "metrics", - "models", - "observations", - "organizations", - "projects", - "prompt_version", - "prompts", - "scim", - "score", - "score_configs", - "score_v_2", - "sessions", - "trace", - "utils", -] diff --git a/langfuse/api/resources/annotation_queues/__init__.py b/langfuse/api/resources/annotation_queues/__init__.py deleted file mode 100644 index 50f79e893..000000000 --- a/langfuse/api/resources/annotation_queues/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - AnnotationQueue, - AnnotationQueueItem, - AnnotationQueueObjectType, - AnnotationQueueStatus, - CreateAnnotationQueueItemRequest, - DeleteAnnotationQueueItemResponse, - PaginatedAnnotationQueueItems, - PaginatedAnnotationQueues, - UpdateAnnotationQueueItemRequest, -) - -__all__ = [ - "AnnotationQueue", - "AnnotationQueueItem", - "AnnotationQueueObjectType", - "AnnotationQueueStatus", - "CreateAnnotationQueueItemRequest", - "DeleteAnnotationQueueItemResponse", - "PaginatedAnnotationQueueItems", - "PaginatedAnnotationQueues", - "UpdateAnnotationQueueItemRequest", -] diff --git a/langfuse/api/resources/annotation_queues/client.py b/langfuse/api/resources/annotation_queues/client.py deleted file mode 100644 index bc1fd287f..000000000 --- a/langfuse/api/resources/annotation_queues/client.py +++ /dev/null @@ -1,1148 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from .types.annotation_queue import AnnotationQueue -from .types.annotation_queue_item import AnnotationQueueItem -from .types.annotation_queue_status import AnnotationQueueStatus -from .types.create_annotation_queue_item_request import CreateAnnotationQueueItemRequest -from .types.delete_annotation_queue_item_response import ( - DeleteAnnotationQueueItemResponse, -) -from .types.paginated_annotation_queue_items import PaginatedAnnotationQueueItems -from .types.paginated_annotation_queues import PaginatedAnnotationQueues -from .types.update_annotation_queue_item_request import UpdateAnnotationQueueItemRequest - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class AnnotationQueuesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list_queues( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedAnnotationQueues: - """ - Get all annotation queues - - Parameters - ---------- - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedAnnotationQueues - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.annotation_queues.list_queues() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/annotation-queues", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - PaginatedAnnotationQueues, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_queue( - self, queue_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> AnnotationQueue: - """ - Get an annotation queue by ID - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AnnotationQueue - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.annotation_queues.get_queue( - queue_id="queueId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(AnnotationQueue, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_queue_items( - self, - queue_id: str, - *, - status: typing.Optional[AnnotationQueueStatus] = None, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedAnnotationQueueItems: - """ - Get items for a specific annotation queue - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - status : typing.Optional[AnnotationQueueStatus] - Filter by status - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedAnnotationQueueItems - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.annotation_queues.list_queue_items( - queue_id="queueId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items", - method="GET", - params={"status": status, "page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - PaginatedAnnotationQueueItems, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_queue_item( - self, - queue_id: str, - item_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> AnnotationQueueItem: - """ - Get a specific item from an annotation queue - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - item_id : str - The unique identifier of the annotation queue item - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AnnotationQueueItem - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.annotation_queues.get_queue_item( - queue_id="queueId", - item_id="itemId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(AnnotationQueueItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create_queue_item( - self, - queue_id: str, - *, - request: CreateAnnotationQueueItemRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> AnnotationQueueItem: - """ - Add an item to an annotation queue - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - request : CreateAnnotationQueueItemRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AnnotationQueueItem - - Examples - -------- - from langfuse import AnnotationQueueObjectType, CreateAnnotationQueueItemRequest - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.annotation_queues.create_queue_item( - queue_id="queueId", - request=CreateAnnotationQueueItemRequest( - object_id="objectId", - object_type=AnnotationQueueObjectType.TRACE, - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(AnnotationQueueItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_queue_item( - self, - queue_id: str, - item_id: str, - *, - request: UpdateAnnotationQueueItemRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> AnnotationQueueItem: - """ - Update an annotation queue item - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - item_id : str - The unique identifier of the annotation queue item - - request : UpdateAnnotationQueueItemRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AnnotationQueueItem - - Examples - -------- - from langfuse import UpdateAnnotationQueueItemRequest - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.annotation_queues.update_queue_item( - queue_id="queueId", - item_id="itemId", - request=UpdateAnnotationQueueItemRequest(), - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", - method="PATCH", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(AnnotationQueueItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_queue_item( - self, - queue_id: str, - item_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DeleteAnnotationQueueItemResponse: - """ - Remove an item from an annotation queue - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - item_id : str - The unique identifier of the annotation queue item - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteAnnotationQueueItemResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.annotation_queues.delete_queue_item( - queue_id="queueId", - item_id="itemId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - DeleteAnnotationQueueItemResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncAnnotationQueuesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list_queues( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedAnnotationQueues: - """ - Get all annotation queues - - Parameters - ---------- - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedAnnotationQueues - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.annotation_queues.list_queues() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/annotation-queues", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - PaginatedAnnotationQueues, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_queue( - self, queue_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> AnnotationQueue: - """ - Get an annotation queue by ID - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AnnotationQueue - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.annotation_queues.get_queue( - queue_id="queueId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(AnnotationQueue, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_queue_items( - self, - queue_id: str, - *, - status: typing.Optional[AnnotationQueueStatus] = None, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedAnnotationQueueItems: - """ - Get items for a specific annotation queue - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - status : typing.Optional[AnnotationQueueStatus] - Filter by status - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedAnnotationQueueItems - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.annotation_queues.list_queue_items( - queue_id="queueId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items", - method="GET", - params={"status": status, "page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - PaginatedAnnotationQueueItems, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_queue_item( - self, - queue_id: str, - item_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> AnnotationQueueItem: - """ - Get a specific item from an annotation queue - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - item_id : str - The unique identifier of the annotation queue item - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AnnotationQueueItem - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.annotation_queues.get_queue_item( - queue_id="queueId", - item_id="itemId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(AnnotationQueueItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create_queue_item( - self, - queue_id: str, - *, - request: CreateAnnotationQueueItemRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> AnnotationQueueItem: - """ - Add an item to an annotation queue - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - request : CreateAnnotationQueueItemRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AnnotationQueueItem - - Examples - -------- - import asyncio - - from langfuse import AnnotationQueueObjectType, CreateAnnotationQueueItemRequest - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.annotation_queues.create_queue_item( - queue_id="queueId", - request=CreateAnnotationQueueItemRequest( - object_id="objectId", - object_type=AnnotationQueueObjectType.TRACE, - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(AnnotationQueueItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_queue_item( - self, - queue_id: str, - item_id: str, - *, - request: UpdateAnnotationQueueItemRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> AnnotationQueueItem: - """ - Update an annotation queue item - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - item_id : str - The unique identifier of the annotation queue item - - request : UpdateAnnotationQueueItemRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AnnotationQueueItem - - Examples - -------- - import asyncio - - from langfuse import UpdateAnnotationQueueItemRequest - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.annotation_queues.update_queue_item( - queue_id="queueId", - item_id="itemId", - request=UpdateAnnotationQueueItemRequest(), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", - method="PATCH", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(AnnotationQueueItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_queue_item( - self, - queue_id: str, - item_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DeleteAnnotationQueueItemResponse: - """ - Remove an item from an annotation queue - - Parameters - ---------- - queue_id : str - The unique identifier of the annotation queue - - item_id : str - The unique identifier of the annotation queue item - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteAnnotationQueueItemResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.annotation_queues.delete_queue_item( - queue_id="queueId", - item_id="itemId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/annotation-queues/{jsonable_encoder(queue_id)}/items/{jsonable_encoder(item_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - DeleteAnnotationQueueItemResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/annotation_queues/types/__init__.py b/langfuse/api/resources/annotation_queues/types/__init__.py deleted file mode 100644 index 110b991cf..000000000 --- a/langfuse/api/resources/annotation_queues/types/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .annotation_queue import AnnotationQueue -from .annotation_queue_item import AnnotationQueueItem -from .annotation_queue_object_type import AnnotationQueueObjectType -from .annotation_queue_status import AnnotationQueueStatus -from .create_annotation_queue_item_request import CreateAnnotationQueueItemRequest -from .delete_annotation_queue_item_response import DeleteAnnotationQueueItemResponse -from .paginated_annotation_queue_items import PaginatedAnnotationQueueItems -from .paginated_annotation_queues import PaginatedAnnotationQueues -from .update_annotation_queue_item_request import UpdateAnnotationQueueItemRequest - -__all__ = [ - "AnnotationQueue", - "AnnotationQueueItem", - "AnnotationQueueObjectType", - "AnnotationQueueStatus", - "CreateAnnotationQueueItemRequest", - "DeleteAnnotationQueueItemResponse", - "PaginatedAnnotationQueueItems", - "PaginatedAnnotationQueues", - "UpdateAnnotationQueueItemRequest", -] diff --git a/langfuse/api/resources/annotation_queues/types/annotation_queue.py b/langfuse/api/resources/annotation_queues/types/annotation_queue.py deleted file mode 100644 index c4cc23282..000000000 --- a/langfuse/api/resources/annotation_queues/types/annotation_queue.py +++ /dev/null @@ -1,49 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class AnnotationQueue(pydantic_v1.BaseModel): - id: str - name: str - description: typing.Optional[str] = None - score_config_ids: typing.List[str] = pydantic_v1.Field(alias="scoreConfigIds") - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/annotation_queues/types/annotation_queue_item.py b/langfuse/api/resources/annotation_queues/types/annotation_queue_item.py deleted file mode 100644 index e88829a1f..000000000 --- a/langfuse/api/resources/annotation_queues/types/annotation_queue_item.py +++ /dev/null @@ -1,55 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .annotation_queue_object_type import AnnotationQueueObjectType -from .annotation_queue_status import AnnotationQueueStatus - - -class AnnotationQueueItem(pydantic_v1.BaseModel): - id: str - queue_id: str = pydantic_v1.Field(alias="queueId") - object_id: str = pydantic_v1.Field(alias="objectId") - object_type: AnnotationQueueObjectType = pydantic_v1.Field(alias="objectType") - status: AnnotationQueueStatus - completed_at: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="completedAt", default=None - ) - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/annotation_queues/types/create_annotation_queue_item_request.py b/langfuse/api/resources/annotation_queues/types/create_annotation_queue_item_request.py deleted file mode 100644 index cbf257f29..000000000 --- a/langfuse/api/resources/annotation_queues/types/create_annotation_queue_item_request.py +++ /dev/null @@ -1,51 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .annotation_queue_object_type import AnnotationQueueObjectType -from .annotation_queue_status import AnnotationQueueStatus - - -class CreateAnnotationQueueItemRequest(pydantic_v1.BaseModel): - object_id: str = pydantic_v1.Field(alias="objectId") - object_type: AnnotationQueueObjectType = pydantic_v1.Field(alias="objectType") - status: typing.Optional[AnnotationQueueStatus] = pydantic_v1.Field(default=None) - """ - Defaults to PENDING for new queue items - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/annotation_queues/types/delete_annotation_queue_item_response.py b/langfuse/api/resources/annotation_queues/types/delete_annotation_queue_item_response.py deleted file mode 100644 index a412c85b7..000000000 --- a/langfuse/api/resources/annotation_queues/types/delete_annotation_queue_item_response.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class DeleteAnnotationQueueItemResponse(pydantic_v1.BaseModel): - success: bool - message: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/annotation_queues/types/paginated_annotation_queue_items.py b/langfuse/api/resources/annotation_queues/types/paginated_annotation_queue_items.py deleted file mode 100644 index 587188d89..000000000 --- a/langfuse/api/resources/annotation_queues/types/paginated_annotation_queue_items.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...utils.resources.pagination.types.meta_response import MetaResponse -from .annotation_queue_item import AnnotationQueueItem - - -class PaginatedAnnotationQueueItems(pydantic_v1.BaseModel): - data: typing.List[AnnotationQueueItem] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/annotation_queues/types/paginated_annotation_queues.py b/langfuse/api/resources/annotation_queues/types/paginated_annotation_queues.py deleted file mode 100644 index aba338414..000000000 --- a/langfuse/api/resources/annotation_queues/types/paginated_annotation_queues.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...utils.resources.pagination.types.meta_response import MetaResponse -from .annotation_queue import AnnotationQueue - - -class PaginatedAnnotationQueues(pydantic_v1.BaseModel): - data: typing.List[AnnotationQueue] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/annotation_queues/types/update_annotation_queue_item_request.py b/langfuse/api/resources/annotation_queues/types/update_annotation_queue_item_request.py deleted file mode 100644 index 3b1c130fe..000000000 --- a/langfuse/api/resources/annotation_queues/types/update_annotation_queue_item_request.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .annotation_queue_status import AnnotationQueueStatus - - -class UpdateAnnotationQueueItemRequest(pydantic_v1.BaseModel): - status: typing.Optional[AnnotationQueueStatus] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/comments/__init__.py b/langfuse/api/resources/comments/__init__.py deleted file mode 100644 index e40c8546f..000000000 --- a/langfuse/api/resources/comments/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateCommentRequest, CreateCommentResponse, GetCommentsResponse - -__all__ = ["CreateCommentRequest", "CreateCommentResponse", "GetCommentsResponse"] diff --git a/langfuse/api/resources/comments/client.py b/langfuse/api/resources/comments/client.py deleted file mode 100644 index 9c78ca23f..000000000 --- a/langfuse/api/resources/comments/client.py +++ /dev/null @@ -1,520 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.comment import Comment -from .types.create_comment_request import CreateCommentRequest -from .types.create_comment_response import CreateCommentResponse -from .types.get_comments_response import GetCommentsResponse - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class CommentsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def create( - self, - *, - request: CreateCommentRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateCommentResponse: - """ - Create a comment. Comments may be attached to different object types (trace, observation, session, prompt). - - Parameters - ---------- - request : CreateCommentRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreateCommentResponse - - Examples - -------- - from langfuse import CreateCommentRequest - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.comments.create( - request=CreateCommentRequest( - project_id="projectId", - object_type="objectType", - object_id="objectId", - content="content", - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/comments", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(CreateCommentResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - object_type: typing.Optional[str] = None, - object_id: typing.Optional[str] = None, - author_user_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> GetCommentsResponse: - """ - Get all comments - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit - - object_type : typing.Optional[str] - Filter comments by object type (trace, observation, session, prompt). - - object_id : typing.Optional[str] - Filter comments by object id. If objectType is not provided, an error will be thrown. - - author_user_id : typing.Optional[str] - Filter comments by author user id. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetCommentsResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.comments.get() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/comments", - method="GET", - params={ - "page": page, - "limit": limit, - "objectType": object_type, - "objectId": object_id, - "authorUserId": author_user_id, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(GetCommentsResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_by_id( - self, - comment_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> Comment: - """ - Get a comment by id - - Parameters - ---------- - comment_id : str - The unique langfuse identifier of a comment - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Comment - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.comments.get_by_id( - comment_id="commentId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/comments/{jsonable_encoder(comment_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Comment, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncCommentsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def create( - self, - *, - request: CreateCommentRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateCommentResponse: - """ - Create a comment. Comments may be attached to different object types (trace, observation, session, prompt). - - Parameters - ---------- - request : CreateCommentRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreateCommentResponse - - Examples - -------- - import asyncio - - from langfuse import CreateCommentRequest - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.comments.create( - request=CreateCommentRequest( - project_id="projectId", - object_type="objectType", - object_id="objectId", - content="content", - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/comments", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(CreateCommentResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - object_type: typing.Optional[str] = None, - object_id: typing.Optional[str] = None, - author_user_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> GetCommentsResponse: - """ - Get all comments - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit - - object_type : typing.Optional[str] - Filter comments by object type (trace, observation, session, prompt). - - object_id : typing.Optional[str] - Filter comments by object id. If objectType is not provided, an error will be thrown. - - author_user_id : typing.Optional[str] - Filter comments by author user id. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetCommentsResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.comments.get() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/comments", - method="GET", - params={ - "page": page, - "limit": limit, - "objectType": object_type, - "objectId": object_id, - "authorUserId": author_user_id, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(GetCommentsResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_by_id( - self, - comment_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> Comment: - """ - Get a comment by id - - Parameters - ---------- - comment_id : str - The unique langfuse identifier of a comment - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Comment - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.comments.get_by_id( - comment_id="commentId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/comments/{jsonable_encoder(comment_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Comment, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/comments/types/__init__.py b/langfuse/api/resources/comments/types/__init__.py deleted file mode 100644 index 13dc1d8d9..000000000 --- a/langfuse/api/resources/comments/types/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_comment_request import CreateCommentRequest -from .create_comment_response import CreateCommentResponse -from .get_comments_response import GetCommentsResponse - -__all__ = ["CreateCommentRequest", "CreateCommentResponse", "GetCommentsResponse"] diff --git a/langfuse/api/resources/comments/types/create_comment_request.py b/langfuse/api/resources/comments/types/create_comment_request.py deleted file mode 100644 index 9ba6081ee..000000000 --- a/langfuse/api/resources/comments/types/create_comment_request.py +++ /dev/null @@ -1,69 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class CreateCommentRequest(pydantic_v1.BaseModel): - project_id: str = pydantic_v1.Field(alias="projectId") - """ - The id of the project to attach the comment to. - """ - - object_type: str = pydantic_v1.Field(alias="objectType") - """ - The type of the object to attach the comment to (trace, observation, session, prompt). - """ - - object_id: str = pydantic_v1.Field(alias="objectId") - """ - The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown. - """ - - content: str = pydantic_v1.Field() - """ - The content of the comment. May include markdown. Currently limited to 3000 characters. - """ - - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - """ - The id of the user who created the comment. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/comments/types/create_comment_response.py b/langfuse/api/resources/comments/types/create_comment_response.py deleted file mode 100644 index d7708f798..000000000 --- a/langfuse/api/resources/comments/types/create_comment_response.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class CreateCommentResponse(pydantic_v1.BaseModel): - id: str = pydantic_v1.Field() - """ - The id of the created object in Langfuse - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/comments/types/get_comments_response.py b/langfuse/api/resources/comments/types/get_comments_response.py deleted file mode 100644 index 66a8b9527..000000000 --- a/langfuse/api/resources/comments/types/get_comments_response.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.comment import Comment -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class GetCommentsResponse(pydantic_v1.BaseModel): - data: typing.List[Comment] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/__init__.py b/langfuse/api/resources/commons/__init__.py deleted file mode 100644 index 6dfbecafe..000000000 --- a/langfuse/api/resources/commons/__init__.py +++ /dev/null @@ -1,103 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - BaseScore, - BaseScoreV1, - BooleanScore, - BooleanScoreV1, - CategoricalScore, - CategoricalScoreV1, - Comment, - CommentObjectType, - ConfigCategory, - CreateScoreValue, - Dataset, - DatasetItem, - DatasetRun, - DatasetRunItem, - DatasetRunWithItems, - DatasetStatus, - MapValue, - Model, - ModelPrice, - ModelUsageUnit, - NumericScore, - NumericScoreV1, - Observation, - ObservationLevel, - ObservationsView, - Score, - ScoreConfig, - ScoreDataType, - ScoreSource, - ScoreV1, - ScoreV1_Boolean, - ScoreV1_Categorical, - ScoreV1_Numeric, - Score_Boolean, - Score_Categorical, - Score_Numeric, - Session, - SessionWithTraces, - Trace, - TraceWithDetails, - TraceWithFullDetails, - Usage, -) -from .errors import ( - AccessDeniedError, - Error, - MethodNotAllowedError, - NotFoundError, - UnauthorizedError, -) - -__all__ = [ - "AccessDeniedError", - "BaseScore", - "BaseScoreV1", - "BooleanScore", - "BooleanScoreV1", - "CategoricalScore", - "CategoricalScoreV1", - "Comment", - "CommentObjectType", - "ConfigCategory", - "CreateScoreValue", - "Dataset", - "DatasetItem", - "DatasetRun", - "DatasetRunItem", - "DatasetRunWithItems", - "DatasetStatus", - "Error", - "MapValue", - "MethodNotAllowedError", - "Model", - "ModelPrice", - "ModelUsageUnit", - "NotFoundError", - "NumericScore", - "NumericScoreV1", - "Observation", - "ObservationLevel", - "ObservationsView", - "Score", - "ScoreConfig", - "ScoreDataType", - "ScoreSource", - "ScoreV1", - "ScoreV1_Boolean", - "ScoreV1_Categorical", - "ScoreV1_Numeric", - "Score_Boolean", - "Score_Categorical", - "Score_Numeric", - "Session", - "SessionWithTraces", - "Trace", - "TraceWithDetails", - "TraceWithFullDetails", - "UnauthorizedError", - "Usage", -] diff --git a/langfuse/api/resources/commons/errors/__init__.py b/langfuse/api/resources/commons/errors/__init__.py deleted file mode 100644 index 0aef2f92f..000000000 --- a/langfuse/api/resources/commons/errors/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .access_denied_error import AccessDeniedError -from .error import Error -from .method_not_allowed_error import MethodNotAllowedError -from .not_found_error import NotFoundError -from .unauthorized_error import UnauthorizedError - -__all__ = [ - "AccessDeniedError", - "Error", - "MethodNotAllowedError", - "NotFoundError", - "UnauthorizedError", -] diff --git a/langfuse/api/resources/commons/errors/access_denied_error.py b/langfuse/api/resources/commons/errors/access_denied_error.py deleted file mode 100644 index 9114ba9ac..000000000 --- a/langfuse/api/resources/commons/errors/access_denied_error.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ....core.api_error import ApiError - - -class AccessDeniedError(ApiError): - def __init__(self, body: typing.Any): - super().__init__(status_code=403, body=body) diff --git a/langfuse/api/resources/commons/errors/error.py b/langfuse/api/resources/commons/errors/error.py deleted file mode 100644 index 06020120c..000000000 --- a/langfuse/api/resources/commons/errors/error.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ....core.api_error import ApiError - - -class Error(ApiError): - def __init__(self, body: typing.Any): - super().__init__(status_code=400, body=body) diff --git a/langfuse/api/resources/commons/errors/method_not_allowed_error.py b/langfuse/api/resources/commons/errors/method_not_allowed_error.py deleted file mode 100644 index 32731a5c7..000000000 --- a/langfuse/api/resources/commons/errors/method_not_allowed_error.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ....core.api_error import ApiError - - -class MethodNotAllowedError(ApiError): - def __init__(self, body: typing.Any): - super().__init__(status_code=405, body=body) diff --git a/langfuse/api/resources/commons/errors/not_found_error.py b/langfuse/api/resources/commons/errors/not_found_error.py deleted file mode 100644 index 564ffca2c..000000000 --- a/langfuse/api/resources/commons/errors/not_found_error.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ....core.api_error import ApiError - - -class NotFoundError(ApiError): - def __init__(self, body: typing.Any): - super().__init__(status_code=404, body=body) diff --git a/langfuse/api/resources/commons/errors/unauthorized_error.py b/langfuse/api/resources/commons/errors/unauthorized_error.py deleted file mode 100644 index 2997f54f6..000000000 --- a/langfuse/api/resources/commons/errors/unauthorized_error.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ....core.api_error import ApiError - - -class UnauthorizedError(ApiError): - def __init__(self, body: typing.Any): - super().__init__(status_code=401, body=body) diff --git a/langfuse/api/resources/commons/types/__init__.py b/langfuse/api/resources/commons/types/__init__.py deleted file mode 100644 index 1c0d06a8d..000000000 --- a/langfuse/api/resources/commons/types/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .base_score import BaseScore -from .base_score_v_1 import BaseScoreV1 -from .boolean_score import BooleanScore -from .boolean_score_v_1 import BooleanScoreV1 -from .categorical_score import CategoricalScore -from .categorical_score_v_1 import CategoricalScoreV1 -from .comment import Comment -from .comment_object_type import CommentObjectType -from .config_category import ConfigCategory -from .create_score_value import CreateScoreValue -from .dataset import Dataset -from .dataset_item import DatasetItem -from .dataset_run import DatasetRun -from .dataset_run_item import DatasetRunItem -from .dataset_run_with_items import DatasetRunWithItems -from .dataset_status import DatasetStatus -from .map_value import MapValue -from .model import Model -from .model_price import ModelPrice -from .model_usage_unit import ModelUsageUnit -from .numeric_score import NumericScore -from .numeric_score_v_1 import NumericScoreV1 -from .observation import Observation -from .observation_level import ObservationLevel -from .observations_view import ObservationsView -from .score import Score, Score_Boolean, Score_Categorical, Score_Numeric -from .score_config import ScoreConfig -from .score_data_type import ScoreDataType -from .score_source import ScoreSource -from .score_v_1 import ScoreV1, ScoreV1_Boolean, ScoreV1_Categorical, ScoreV1_Numeric -from .session import Session -from .session_with_traces import SessionWithTraces -from .trace import Trace -from .trace_with_details import TraceWithDetails -from .trace_with_full_details import TraceWithFullDetails -from .usage import Usage - -__all__ = [ - "BaseScore", - "BaseScoreV1", - "BooleanScore", - "BooleanScoreV1", - "CategoricalScore", - "CategoricalScoreV1", - "Comment", - "CommentObjectType", - "ConfigCategory", - "CreateScoreValue", - "Dataset", - "DatasetItem", - "DatasetRun", - "DatasetRunItem", - "DatasetRunWithItems", - "DatasetStatus", - "MapValue", - "Model", - "ModelPrice", - "ModelUsageUnit", - "NumericScore", - "NumericScoreV1", - "Observation", - "ObservationLevel", - "ObservationsView", - "Score", - "ScoreConfig", - "ScoreDataType", - "ScoreSource", - "ScoreV1", - "ScoreV1_Boolean", - "ScoreV1_Categorical", - "ScoreV1_Numeric", - "Score_Boolean", - "Score_Categorical", - "Score_Numeric", - "Session", - "SessionWithTraces", - "Trace", - "TraceWithDetails", - "TraceWithFullDetails", - "Usage", -] diff --git a/langfuse/api/resources/commons/types/base_score.py b/langfuse/api/resources/commons/types/base_score.py deleted file mode 100644 index 74e93d0bd..000000000 --- a/langfuse/api/resources/commons/types/base_score.py +++ /dev/null @@ -1,79 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .score_source import ScoreSource - - -class BaseScore(pydantic_v1.BaseModel): - id: str - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - """ - Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range - """ - - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - """ - Reference an annotation queue on a score. Populated if the score was initially created in an annotation queue. - """ - - environment: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/base_score_v_1.py b/langfuse/api/resources/commons/types/base_score_v_1.py deleted file mode 100644 index a89a4f7bb..000000000 --- a/langfuse/api/resources/commons/types/base_score_v_1.py +++ /dev/null @@ -1,73 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .score_source import ScoreSource - - -class BaseScoreV1(pydantic_v1.BaseModel): - id: str - trace_id: str = pydantic_v1.Field(alias="traceId") - name: str - source: ScoreSource - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - """ - Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range - """ - - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - """ - Reference an annotation queue on a score. Populated if the score was initially created in an annotation queue. - """ - - environment: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/boolean_score.py b/langfuse/api/resources/commons/types/boolean_score.py deleted file mode 100644 index d838b7db9..000000000 --- a/langfuse/api/resources/commons/types/boolean_score.py +++ /dev/null @@ -1,53 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_score import BaseScore - - -class BooleanScore(BaseScore): - value: float = pydantic_v1.Field() - """ - The numeric value of the score. Equals 1 for "True" and 0 for "False" - """ - - string_value: str = pydantic_v1.Field(alias="stringValue") - """ - The string representation of the score value. Is inferred from the numeric value and equals "True" or "False" - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/boolean_score_v_1.py b/langfuse/api/resources/commons/types/boolean_score_v_1.py deleted file mode 100644 index 9f8e8935f..000000000 --- a/langfuse/api/resources/commons/types/boolean_score_v_1.py +++ /dev/null @@ -1,53 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_score_v_1 import BaseScoreV1 - - -class BooleanScoreV1(BaseScoreV1): - value: float = pydantic_v1.Field() - """ - The numeric value of the score. Equals 1 for "True" and 0 for "False" - """ - - string_value: str = pydantic_v1.Field(alias="stringValue") - """ - The string representation of the score value. Is inferred from the numeric value and equals "True" or "False" - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/categorical_score.py b/langfuse/api/resources/commons/types/categorical_score.py deleted file mode 100644 index 847caf47b..000000000 --- a/langfuse/api/resources/commons/types/categorical_score.py +++ /dev/null @@ -1,53 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_score import BaseScore - - -class CategoricalScore(BaseScore): - value: typing.Optional[float] = pydantic_v1.Field(default=None) - """ - Only defined if a config is linked. Represents the numeric category mapping of the stringValue - """ - - string_value: str = pydantic_v1.Field(alias="stringValue") - """ - The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/categorical_score_v_1.py b/langfuse/api/resources/commons/types/categorical_score_v_1.py deleted file mode 100644 index 2cc84fc25..000000000 --- a/langfuse/api/resources/commons/types/categorical_score_v_1.py +++ /dev/null @@ -1,53 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_score_v_1 import BaseScoreV1 - - -class CategoricalScoreV1(BaseScoreV1): - value: typing.Optional[float] = pydantic_v1.Field(default=None) - """ - Only defined if a config is linked. Represents the numeric category mapping of the stringValue - """ - - string_value: str = pydantic_v1.Field(alias="stringValue") - """ - The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/comment.py b/langfuse/api/resources/commons/types/comment.py deleted file mode 100644 index 4d8b1916a..000000000 --- a/langfuse/api/resources/commons/types/comment.py +++ /dev/null @@ -1,54 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .comment_object_type import CommentObjectType - - -class Comment(pydantic_v1.BaseModel): - id: str - project_id: str = pydantic_v1.Field(alias="projectId") - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - object_type: CommentObjectType = pydantic_v1.Field(alias="objectType") - object_id: str = pydantic_v1.Field(alias="objectId") - content: str - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/config_category.py b/langfuse/api/resources/commons/types/config_category.py deleted file mode 100644 index b1cbde9f2..000000000 --- a/langfuse/api/resources/commons/types/config_category.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ConfigCategory(pydantic_v1.BaseModel): - value: float - label: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/dataset.py b/langfuse/api/resources/commons/types/dataset.py deleted file mode 100644 index be59a951a..000000000 --- a/langfuse/api/resources/commons/types/dataset.py +++ /dev/null @@ -1,50 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class Dataset(pydantic_v1.BaseModel): - id: str - name: str - description: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - project_id: str = pydantic_v1.Field(alias="projectId") - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/dataset_item.py b/langfuse/api/resources/commons/types/dataset_item.py deleted file mode 100644 index dd5f85e78..000000000 --- a/langfuse/api/resources/commons/types/dataset_item.py +++ /dev/null @@ -1,61 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .dataset_status import DatasetStatus - - -class DatasetItem(pydantic_v1.BaseModel): - id: str - status: DatasetStatus - input: typing.Optional[typing.Any] = None - expected_output: typing.Optional[typing.Any] = pydantic_v1.Field( - alias="expectedOutput", default=None - ) - metadata: typing.Optional[typing.Any] = None - source_trace_id: typing.Optional[str] = pydantic_v1.Field( - alias="sourceTraceId", default=None - ) - source_observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="sourceObservationId", default=None - ) - dataset_id: str = pydantic_v1.Field(alias="datasetId") - dataset_name: str = pydantic_v1.Field(alias="datasetName") - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/dataset_run.py b/langfuse/api/resources/commons/types/dataset_run.py deleted file mode 100644 index 74b1a2ac8..000000000 --- a/langfuse/api/resources/commons/types/dataset_run.py +++ /dev/null @@ -1,82 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class DatasetRun(pydantic_v1.BaseModel): - id: str = pydantic_v1.Field() - """ - Unique identifier of the dataset run - """ - - name: str = pydantic_v1.Field() - """ - Name of the dataset run - """ - - description: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - Description of the run - """ - - metadata: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - Metadata of the dataset run - """ - - dataset_id: str = pydantic_v1.Field(alias="datasetId") - """ - Id of the associated dataset - """ - - dataset_name: str = pydantic_v1.Field(alias="datasetName") - """ - Name of the associated dataset - """ - - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - """ - The date and time when the dataset run was created - """ - - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - """ - The date and time when the dataset run was last updated - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/dataset_run_item.py b/langfuse/api/resources/commons/types/dataset_run_item.py deleted file mode 100644 index f1b3af163..000000000 --- a/langfuse/api/resources/commons/types/dataset_run_item.py +++ /dev/null @@ -1,53 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class DatasetRunItem(pydantic_v1.BaseModel): - id: str - dataset_run_id: str = pydantic_v1.Field(alias="datasetRunId") - dataset_run_name: str = pydantic_v1.Field(alias="datasetRunName") - dataset_item_id: str = pydantic_v1.Field(alias="datasetItemId") - trace_id: str = pydantic_v1.Field(alias="traceId") - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/dataset_run_with_items.py b/langfuse/api/resources/commons/types/dataset_run_with_items.py deleted file mode 100644 index 647d2c553..000000000 --- a/langfuse/api/resources/commons/types/dataset_run_with_items.py +++ /dev/null @@ -1,48 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .dataset_run import DatasetRun -from .dataset_run_item import DatasetRunItem - - -class DatasetRunWithItems(DatasetRun): - dataset_run_items: typing.List[DatasetRunItem] = pydantic_v1.Field( - alias="datasetRunItems" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/model.py b/langfuse/api/resources/commons/types/model.py deleted file mode 100644 index ea3922ee9..000000000 --- a/langfuse/api/resources/commons/types/model.py +++ /dev/null @@ -1,112 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .model_price import ModelPrice -from .model_usage_unit import ModelUsageUnit - - -class Model(pydantic_v1.BaseModel): - """ - Model definition used for transforming usage into USD cost and/or tokenization. - """ - - id: str - model_name: str = pydantic_v1.Field(alias="modelName") - """ - Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/model_price.py b/langfuse/api/resources/commons/types/model_price.py deleted file mode 100644 index 8882004e7..000000000 --- a/langfuse/api/resources/commons/types/model_price.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ModelPrice(pydantic_v1.BaseModel): - price: float - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/numeric_score.py b/langfuse/api/resources/commons/types/numeric_score.py deleted file mode 100644 index d7f860cd5..000000000 --- a/langfuse/api/resources/commons/types/numeric_score.py +++ /dev/null @@ -1,48 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_score import BaseScore - - -class NumericScore(BaseScore): - value: float = pydantic_v1.Field() - """ - The numeric value of the score - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/numeric_score_v_1.py b/langfuse/api/resources/commons/types/numeric_score_v_1.py deleted file mode 100644 index 773d84b46..000000000 --- a/langfuse/api/resources/commons/types/numeric_score_v_1.py +++ /dev/null @@ -1,48 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_score_v_1 import BaseScoreV1 - - -class NumericScoreV1(BaseScoreV1): - value: float = pydantic_v1.Field() - """ - The numeric value of the score - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/observation.py b/langfuse/api/resources/commons/types/observation.py deleted file mode 100644 index b821476f9..000000000 --- a/langfuse/api/resources/commons/types/observation.py +++ /dev/null @@ -1,164 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .map_value import MapValue -from .observation_level import ObservationLevel -from .usage import Usage - - -class Observation(pydantic_v1.BaseModel): - id: str = pydantic_v1.Field() - """ - The unique identifier of the observation - """ - - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - """ - The trace ID associated with the observation - """ - - type: str = pydantic_v1.Field() - """ - The type of the observation - """ - - name: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The name of the observation - """ - - start_time: dt.datetime = pydantic_v1.Field(alias="startTime") - """ - The start time of the observation - """ - - end_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="endTime", default=None - ) - """ - The end time of the observation. - """ - - completion_start_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="completionStartTime", default=None - ) - """ - The completion start time of the observation - """ - - model: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The model used for the observation - """ - - model_parameters: typing.Optional[typing.Dict[str, MapValue]] = pydantic_v1.Field( - alias="modelParameters", default=None - ) - """ - The parameters of the model used for the observation - """ - - input: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - The input data of the observation - """ - - version: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The version of the observation - """ - - metadata: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - Additional metadata of the observation - """ - - output: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - The output data of the observation - """ - - usage: typing.Optional[Usage] = pydantic_v1.Field(default=None) - """ - (Deprecated. Use usageDetails and costDetails instead.) The usage data of the observation - """ - - level: ObservationLevel = pydantic_v1.Field() - """ - The level of the observation - """ - - status_message: typing.Optional[str] = pydantic_v1.Field( - alias="statusMessage", default=None - ) - """ - The status message of the observation - """ - - parent_observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="parentObservationId", default=None - ) - """ - The parent observation ID - """ - - prompt_id: typing.Optional[str] = pydantic_v1.Field(alias="promptId", default=None) - """ - The prompt ID associated with the observation - """ - - usage_details: typing.Optional[typing.Dict[str, int]] = pydantic_v1.Field( - alias="usageDetails", default=None - ) - """ - The usage details of the observation. Key is the name of the usage metric, value is the number of units consumed. The total key is the sum of all (non-total) usage metrics or the total value ingested. - """ - - cost_details: typing.Optional[typing.Dict[str, float]] = pydantic_v1.Field( - alias="costDetails", default=None - ) - """ - The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested. - """ - - environment: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The environment from which this observation originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/observations_view.py b/langfuse/api/resources/commons/types/observations_view.py deleted file mode 100644 index e011fa32b..000000000 --- a/langfuse/api/resources/commons/types/observations_view.py +++ /dev/null @@ -1,116 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .observation import Observation - - -class ObservationsView(Observation): - prompt_name: typing.Optional[str] = pydantic_v1.Field( - alias="promptName", default=None - ) - """ - The name of the prompt associated with the observation - """ - - prompt_version: typing.Optional[int] = pydantic_v1.Field( - alias="promptVersion", default=None - ) - """ - The version of the prompt associated with the observation - """ - - model_id: typing.Optional[str] = pydantic_v1.Field(alias="modelId", default=None) - """ - The unique identifier of the model - """ - - input_price: typing.Optional[float] = pydantic_v1.Field( - alias="inputPrice", default=None - ) - """ - The price of the input in USD - """ - - output_price: typing.Optional[float] = pydantic_v1.Field( - alias="outputPrice", default=None - ) - """ - The price of the output in USD. - """ - - total_price: typing.Optional[float] = pydantic_v1.Field( - alias="totalPrice", default=None - ) - """ - The total price in USD. - """ - - calculated_input_cost: typing.Optional[float] = pydantic_v1.Field( - alias="calculatedInputCost", default=None - ) - """ - (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the input in USD - """ - - calculated_output_cost: typing.Optional[float] = pydantic_v1.Field( - alias="calculatedOutputCost", default=None - ) - """ - (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the output in USD - """ - - calculated_total_cost: typing.Optional[float] = pydantic_v1.Field( - alias="calculatedTotalCost", default=None - ) - """ - (Deprecated. Use usageDetails and costDetails instead.) The calculated total cost in USD - """ - - latency: typing.Optional[float] = pydantic_v1.Field(default=None) - """ - The latency in seconds. - """ - - time_to_first_token: typing.Optional[float] = pydantic_v1.Field( - alias="timeToFirstToken", default=None - ) - """ - The time to the first token in seconds - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/score.py b/langfuse/api/resources/commons/types/score.py deleted file mode 100644 index 789854afc..000000000 --- a/langfuse/api/resources/commons/types/score.py +++ /dev/null @@ -1,207 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .score_source import ScoreSource - - -class Score_Numeric(pydantic_v1.BaseModel): - value: float - id: str - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["NUMERIC"] = pydantic_v1.Field( - alias="dataType", default="NUMERIC" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class Score_Categorical(pydantic_v1.BaseModel): - value: typing.Optional[float] = None - string_value: str = pydantic_v1.Field(alias="stringValue") - id: str - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["CATEGORICAL"] = pydantic_v1.Field( - alias="dataType", default="CATEGORICAL" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class Score_Boolean(pydantic_v1.BaseModel): - value: float - string_value: str = pydantic_v1.Field(alias="stringValue") - id: str - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["BOOLEAN"] = pydantic_v1.Field( - alias="dataType", default="BOOLEAN" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -Score = typing.Union[Score_Numeric, Score_Categorical, Score_Boolean] diff --git a/langfuse/api/resources/commons/types/score_config.py b/langfuse/api/resources/commons/types/score_config.py deleted file mode 100644 index 4a7b30e0e..000000000 --- a/langfuse/api/resources/commons/types/score_config.py +++ /dev/null @@ -1,82 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .config_category import ConfigCategory -from .score_data_type import ScoreDataType - - -class ScoreConfig(pydantic_v1.BaseModel): - """ - Configuration for a score - """ - - id: str - name: str - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - project_id: str = pydantic_v1.Field(alias="projectId") - data_type: ScoreDataType = pydantic_v1.Field(alias="dataType") - is_archived: bool = pydantic_v1.Field(alias="isArchived") - """ - Whether the score config is archived. Defaults to false - """ - - min_value: typing.Optional[float] = pydantic_v1.Field( - alias="minValue", default=None - ) - """ - Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞ - """ - - max_value: typing.Optional[float] = pydantic_v1.Field( - alias="maxValue", default=None - ) - """ - Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞ - """ - - categories: typing.Optional[typing.List[ConfigCategory]] = pydantic_v1.Field( - default=None - ) - """ - Configures custom categories for categorical scores - """ - - description: typing.Optional[str] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/score_v_1.py b/langfuse/api/resources/commons/types/score_v_1.py deleted file mode 100644 index 788f6f39c..000000000 --- a/langfuse/api/resources/commons/types/score_v_1.py +++ /dev/null @@ -1,189 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .score_source import ScoreSource - - -class ScoreV1_Numeric(pydantic_v1.BaseModel): - value: float - id: str - trace_id: str = pydantic_v1.Field(alias="traceId") - name: str - source: ScoreSource - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["NUMERIC"] = pydantic_v1.Field( - alias="dataType", default="NUMERIC" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class ScoreV1_Categorical(pydantic_v1.BaseModel): - value: typing.Optional[float] = None - string_value: str = pydantic_v1.Field(alias="stringValue") - id: str - trace_id: str = pydantic_v1.Field(alias="traceId") - name: str - source: ScoreSource - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["CATEGORICAL"] = pydantic_v1.Field( - alias="dataType", default="CATEGORICAL" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class ScoreV1_Boolean(pydantic_v1.BaseModel): - value: float - string_value: str = pydantic_v1.Field(alias="stringValue") - id: str - trace_id: str = pydantic_v1.Field(alias="traceId") - name: str - source: ScoreSource - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["BOOLEAN"] = pydantic_v1.Field( - alias="dataType", default="BOOLEAN" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -ScoreV1 = typing.Union[ScoreV1_Numeric, ScoreV1_Categorical, ScoreV1_Boolean] diff --git a/langfuse/api/resources/commons/types/session.py b/langfuse/api/resources/commons/types/session.py deleted file mode 100644 index 46a0a6b96..000000000 --- a/langfuse/api/resources/commons/types/session.py +++ /dev/null @@ -1,50 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class Session(pydantic_v1.BaseModel): - id: str - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - project_id: str = pydantic_v1.Field(alias="projectId") - environment: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The environment from which this session originated. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/session_with_traces.py b/langfuse/api/resources/commons/types/session_with_traces.py deleted file mode 100644 index b5465daa9..000000000 --- a/langfuse/api/resources/commons/types/session_with_traces.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .session import Session -from .trace import Trace - - -class SessionWithTraces(Session): - traces: typing.List[Trace] - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/trace.py b/langfuse/api/resources/commons/types/trace.py deleted file mode 100644 index d977ed3d7..000000000 --- a/langfuse/api/resources/commons/types/trace.py +++ /dev/null @@ -1,109 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class Trace(pydantic_v1.BaseModel): - id: str = pydantic_v1.Field() - """ - The unique identifier of a trace - """ - - timestamp: dt.datetime = pydantic_v1.Field() - """ - The timestamp when the trace was created - """ - - name: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The name of the trace - """ - - input: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - The input data of the trace. Can be any JSON. - """ - - output: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - The output data of the trace. Can be any JSON. - """ - - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - """ - The session identifier associated with the trace - """ - - release: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The release version of the application when the trace was created - """ - - version: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The version of the trace - """ - - user_id: typing.Optional[str] = pydantic_v1.Field(alias="userId", default=None) - """ - The user identifier associated with the trace - """ - - metadata: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - The metadata associated with the trace. Can be any JSON. - """ - - tags: typing.Optional[typing.List[str]] = pydantic_v1.Field(default=None) - """ - The tags associated with the trace. Can be an array of strings or null. - """ - - public: typing.Optional[bool] = pydantic_v1.Field(default=None) - """ - Public traces are accessible via url without login - """ - - environment: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/trace_with_details.py b/langfuse/api/resources/commons/types/trace_with_details.py deleted file mode 100644 index 5ffe6f218..000000000 --- a/langfuse/api/resources/commons/types/trace_with_details.py +++ /dev/null @@ -1,68 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .trace import Trace - - -class TraceWithDetails(Trace): - html_path: str = pydantic_v1.Field(alias="htmlPath") - """ - Path of trace in Langfuse UI - """ - - latency: float = pydantic_v1.Field() - """ - Latency of trace in seconds - """ - - total_cost: float = pydantic_v1.Field(alias="totalCost") - """ - Cost of trace in USD - """ - - observations: typing.List[str] = pydantic_v1.Field() - """ - List of observation ids - """ - - scores: typing.List[str] = pydantic_v1.Field() - """ - List of score ids - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/trace_with_full_details.py b/langfuse/api/resources/commons/types/trace_with_full_details.py deleted file mode 100644 index 2c6a99402..000000000 --- a/langfuse/api/resources/commons/types/trace_with_full_details.py +++ /dev/null @@ -1,70 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .observations_view import ObservationsView -from .score_v_1 import ScoreV1 -from .trace import Trace - - -class TraceWithFullDetails(Trace): - html_path: str = pydantic_v1.Field(alias="htmlPath") - """ - Path of trace in Langfuse UI - """ - - latency: float = pydantic_v1.Field() - """ - Latency of trace in seconds - """ - - total_cost: float = pydantic_v1.Field(alias="totalCost") - """ - Cost of trace in USD - """ - - observations: typing.List[ObservationsView] = pydantic_v1.Field() - """ - List of observations - """ - - scores: typing.List[ScoreV1] = pydantic_v1.Field() - """ - List of scores - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/commons/types/usage.py b/langfuse/api/resources/commons/types/usage.py deleted file mode 100644 index c38330494..000000000 --- a/langfuse/api/resources/commons/types/usage.py +++ /dev/null @@ -1,84 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .model_usage_unit import ModelUsageUnit - - -class Usage(pydantic_v1.BaseModel): - """ - (Deprecated. Use usageDetails and costDetails instead.) Standard interface for usage and cost - """ - - input: typing.Optional[int] = pydantic_v1.Field(default=None) - """ - Number of input units (e.g. tokens) - """ - - output: typing.Optional[int] = pydantic_v1.Field(default=None) - """ - Number of output units (e.g. tokens) - """ - - total: typing.Optional[int] = pydantic_v1.Field(default=None) - """ - Defaults to input+output if not set - """ - - unit: typing.Optional[ModelUsageUnit] = None - input_cost: typing.Optional[float] = pydantic_v1.Field( - alias="inputCost", default=None - ) - """ - USD input cost - """ - - output_cost: typing.Optional[float] = pydantic_v1.Field( - alias="outputCost", default=None - ) - """ - USD output cost - """ - - total_cost: typing.Optional[float] = pydantic_v1.Field( - alias="totalCost", default=None - ) - """ - USD total cost, defaults to input+output - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/dataset_items/__init__.py b/langfuse/api/resources/dataset_items/__init__.py deleted file mode 100644 index 06d2ae527..000000000 --- a/langfuse/api/resources/dataset_items/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - CreateDatasetItemRequest, - DeleteDatasetItemResponse, - PaginatedDatasetItems, -) - -__all__ = [ - "CreateDatasetItemRequest", - "DeleteDatasetItemResponse", - "PaginatedDatasetItems", -] diff --git a/langfuse/api/resources/dataset_items/client.py b/langfuse/api/resources/dataset_items/client.py deleted file mode 100644 index 8ece3a790..000000000 --- a/langfuse/api/resources/dataset_items/client.py +++ /dev/null @@ -1,640 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.dataset_item import DatasetItem -from .types.create_dataset_item_request import CreateDatasetItemRequest -from .types.delete_dataset_item_response import DeleteDatasetItemResponse -from .types.paginated_dataset_items import PaginatedDatasetItems - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class DatasetItemsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def create( - self, - *, - request: CreateDatasetItemRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetItem: - """ - Create a dataset item - - Parameters - ---------- - request : CreateDatasetItemRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetItem - - Examples - -------- - from langfuse import CreateDatasetItemRequest - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.dataset_items.create( - request=CreateDatasetItemRequest( - dataset_name="datasetName", - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/dataset-items", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DatasetItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> DatasetItem: - """ - Get a dataset item - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetItem - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.dataset_items.get( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/dataset-items/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DatasetItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list( - self, - *, - dataset_name: typing.Optional[str] = None, - source_trace_id: typing.Optional[str] = None, - source_observation_id: typing.Optional[str] = None, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetItems: - """ - Get dataset items - - Parameters - ---------- - dataset_name : typing.Optional[str] - - source_trace_id : typing.Optional[str] - - source_observation_id : typing.Optional[str] - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetItems - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.dataset_items.list() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/dataset-items", - method="GET", - params={ - "datasetName": dataset_name, - "sourceTraceId": source_trace_id, - "sourceObservationId": source_observation_id, - "page": page, - "limit": limit, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedDatasetItems, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> DeleteDatasetItemResponse: - """ - Delete a dataset item and all its run items. This action is irreversible. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteDatasetItemResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.dataset_items.delete( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/dataset-items/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - DeleteDatasetItemResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncDatasetItemsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def create( - self, - *, - request: CreateDatasetItemRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetItem: - """ - Create a dataset item - - Parameters - ---------- - request : CreateDatasetItemRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetItem - - Examples - -------- - import asyncio - - from langfuse import CreateDatasetItemRequest - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.dataset_items.create( - request=CreateDatasetItemRequest( - dataset_name="datasetName", - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/dataset-items", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DatasetItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> DatasetItem: - """ - Get a dataset item - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetItem - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.dataset_items.get( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/dataset-items/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DatasetItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list( - self, - *, - dataset_name: typing.Optional[str] = None, - source_trace_id: typing.Optional[str] = None, - source_observation_id: typing.Optional[str] = None, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetItems: - """ - Get dataset items - - Parameters - ---------- - dataset_name : typing.Optional[str] - - source_trace_id : typing.Optional[str] - - source_observation_id : typing.Optional[str] - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetItems - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.dataset_items.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/dataset-items", - method="GET", - params={ - "datasetName": dataset_name, - "sourceTraceId": source_trace_id, - "sourceObservationId": source_observation_id, - "page": page, - "limit": limit, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedDatasetItems, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> DeleteDatasetItemResponse: - """ - Delete a dataset item and all its run items. This action is irreversible. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteDatasetItemResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.dataset_items.delete( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/dataset-items/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - DeleteDatasetItemResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/dataset_items/types/__init__.py b/langfuse/api/resources/dataset_items/types/__init__.py deleted file mode 100644 index 214adce0e..000000000 --- a/langfuse/api/resources/dataset_items/types/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_dataset_item_request import CreateDatasetItemRequest -from .delete_dataset_item_response import DeleteDatasetItemResponse -from .paginated_dataset_items import PaginatedDatasetItems - -__all__ = [ - "CreateDatasetItemRequest", - "DeleteDatasetItemResponse", - "PaginatedDatasetItems", -] diff --git a/langfuse/api/resources/dataset_items/types/create_dataset_item_request.py b/langfuse/api/resources/dataset_items/types/create_dataset_item_request.py deleted file mode 100644 index 111f6819a..000000000 --- a/langfuse/api/resources/dataset_items/types/create_dataset_item_request.py +++ /dev/null @@ -1,65 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.dataset_status import DatasetStatus - - -class CreateDatasetItemRequest(pydantic_v1.BaseModel): - dataset_name: str = pydantic_v1.Field(alias="datasetName") - input: typing.Optional[typing.Any] = None - expected_output: typing.Optional[typing.Any] = pydantic_v1.Field( - alias="expectedOutput", default=None - ) - metadata: typing.Optional[typing.Any] = None - source_trace_id: typing.Optional[str] = pydantic_v1.Field( - alias="sourceTraceId", default=None - ) - source_observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="sourceObservationId", default=None - ) - id: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - Dataset items are upserted on their id. Id needs to be unique (project-level) and cannot be reused across datasets. - """ - - status: typing.Optional[DatasetStatus] = pydantic_v1.Field(default=None) - """ - Defaults to ACTIVE for newly created items - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/dataset_items/types/delete_dataset_item_response.py b/langfuse/api/resources/dataset_items/types/delete_dataset_item_response.py deleted file mode 100644 index 4d700ff75..000000000 --- a/langfuse/api/resources/dataset_items/types/delete_dataset_item_response.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class DeleteDatasetItemResponse(pydantic_v1.BaseModel): - message: str = pydantic_v1.Field() - """ - Success message after deletion - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/dataset_items/types/paginated_dataset_items.py b/langfuse/api/resources/dataset_items/types/paginated_dataset_items.py deleted file mode 100644 index 8592ba80f..000000000 --- a/langfuse/api/resources/dataset_items/types/paginated_dataset_items.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.dataset_item import DatasetItem -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class PaginatedDatasetItems(pydantic_v1.BaseModel): - data: typing.List[DatasetItem] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/dataset_run_items/__init__.py b/langfuse/api/resources/dataset_run_items/__init__.py deleted file mode 100644 index d522a3129..000000000 --- a/langfuse/api/resources/dataset_run_items/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateDatasetRunItemRequest, PaginatedDatasetRunItems - -__all__ = ["CreateDatasetRunItemRequest", "PaginatedDatasetRunItems"] diff --git a/langfuse/api/resources/dataset_run_items/client.py b/langfuse/api/resources/dataset_run_items/client.py deleted file mode 100644 index 3664fde96..000000000 --- a/langfuse/api/resources/dataset_run_items/client.py +++ /dev/null @@ -1,366 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.dataset_run_item import DatasetRunItem -from .types.create_dataset_run_item_request import CreateDatasetRunItemRequest -from .types.paginated_dataset_run_items import PaginatedDatasetRunItems - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class DatasetRunItemsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def create( - self, - *, - request: CreateDatasetRunItemRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetRunItem: - """ - Create a dataset run item - - Parameters - ---------- - request : CreateDatasetRunItemRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetRunItem - - Examples - -------- - from langfuse import CreateDatasetRunItemRequest - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.dataset_run_items.create( - request=CreateDatasetRunItemRequest( - run_name="runName", - dataset_item_id="datasetItemId", - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/dataset-run-items", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DatasetRunItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list( - self, - *, - dataset_id: str, - run_name: str, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetRunItems: - """ - List dataset run items - - Parameters - ---------- - dataset_id : str - - run_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetRunItems - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.dataset_run_items.list( - dataset_id="datasetId", - run_name="runName", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/dataset-run-items", - method="GET", - params={ - "datasetId": dataset_id, - "runName": run_name, - "page": page, - "limit": limit, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - PaginatedDatasetRunItems, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncDatasetRunItemsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def create( - self, - *, - request: CreateDatasetRunItemRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetRunItem: - """ - Create a dataset run item - - Parameters - ---------- - request : CreateDatasetRunItemRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetRunItem - - Examples - -------- - import asyncio - - from langfuse import CreateDatasetRunItemRequest - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.dataset_run_items.create( - request=CreateDatasetRunItemRequest( - run_name="runName", - dataset_item_id="datasetItemId", - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/dataset-run-items", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DatasetRunItem, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list( - self, - *, - dataset_id: str, - run_name: str, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetRunItems: - """ - List dataset run items - - Parameters - ---------- - dataset_id : str - - run_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetRunItems - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.dataset_run_items.list( - dataset_id="datasetId", - run_name="runName", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/dataset-run-items", - method="GET", - params={ - "datasetId": dataset_id, - "runName": run_name, - "page": page, - "limit": limit, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - PaginatedDatasetRunItems, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/dataset_run_items/types/__init__.py b/langfuse/api/resources/dataset_run_items/types/__init__.py deleted file mode 100644 index e48e72c27..000000000 --- a/langfuse/api/resources/dataset_run_items/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_dataset_run_item_request import CreateDatasetRunItemRequest -from .paginated_dataset_run_items import PaginatedDatasetRunItems - -__all__ = ["CreateDatasetRunItemRequest", "PaginatedDatasetRunItems"] diff --git a/langfuse/api/resources/dataset_run_items/types/create_dataset_run_item_request.py b/langfuse/api/resources/dataset_run_items/types/create_dataset_run_item_request.py deleted file mode 100644 index 0a643b835..000000000 --- a/langfuse/api/resources/dataset_run_items/types/create_dataset_run_item_request.py +++ /dev/null @@ -1,64 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class CreateDatasetRunItemRequest(pydantic_v1.BaseModel): - run_name: str = pydantic_v1.Field(alias="runName") - run_description: typing.Optional[str] = pydantic_v1.Field( - alias="runDescription", default=None - ) - """ - Description of the run. If run exists, description will be updated. - """ - - metadata: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - Metadata of the dataset run, updates run if run already exists - """ - - dataset_item_id: str = pydantic_v1.Field(alias="datasetItemId") - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - """ - traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/dataset_run_items/types/paginated_dataset_run_items.py b/langfuse/api/resources/dataset_run_items/types/paginated_dataset_run_items.py deleted file mode 100644 index c1611bae0..000000000 --- a/langfuse/api/resources/dataset_run_items/types/paginated_dataset_run_items.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.dataset_run_item import DatasetRunItem -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class PaginatedDatasetRunItems(pydantic_v1.BaseModel): - data: typing.List[DatasetRunItem] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/datasets/__init__.py b/langfuse/api/resources/datasets/__init__.py deleted file mode 100644 index dd30a359d..000000000 --- a/langfuse/api/resources/datasets/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - CreateDatasetRequest, - DeleteDatasetRunResponse, - PaginatedDatasetRuns, - PaginatedDatasets, -) - -__all__ = [ - "CreateDatasetRequest", - "DeleteDatasetRunResponse", - "PaginatedDatasetRuns", - "PaginatedDatasets", -] diff --git a/langfuse/api/resources/datasets/client.py b/langfuse/api/resources/datasets/client.py deleted file mode 100644 index aff7293a0..000000000 --- a/langfuse/api/resources/datasets/client.py +++ /dev/null @@ -1,942 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.dataset import Dataset -from ..commons.types.dataset_run_with_items import DatasetRunWithItems -from .types.create_dataset_request import CreateDatasetRequest -from .types.delete_dataset_run_response import DeleteDatasetRunResponse -from .types.paginated_dataset_runs import PaginatedDatasetRuns -from .types.paginated_datasets import PaginatedDatasets - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class DatasetsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasets: - """ - Get all datasets - - Parameters - ---------- - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasets - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.list() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/v2/datasets", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedDatasets, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get( - self, - dataset_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> Dataset: - """ - Get a dataset - - Parameters - ---------- - dataset_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Dataset - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.get( - dataset_name="datasetName", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/v2/datasets/{jsonable_encoder(dataset_name)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Dataset, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, - *, - request: CreateDatasetRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> Dataset: - """ - Create a dataset - - Parameters - ---------- - request : CreateDatasetRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Dataset - - Examples - -------- - from langfuse import CreateDatasetRequest - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.create( - request=CreateDatasetRequest( - name="name", - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/v2/datasets", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Dataset, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetRunWithItems: - """ - Get a dataset run and its items - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetRunWithItems - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.get_run( - dataset_name="datasetName", - run_name="runName", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DatasetRunWithItems, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DeleteDatasetRunResponse: - """ - Delete a dataset run and all its run items. This action is irreversible. - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteDatasetRunResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.delete_run( - dataset_name="datasetName", - run_name="runName", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - DeleteDatasetRunResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_runs( - self, - dataset_name: str, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetRuns: - """ - Get dataset runs - - Parameters - ---------- - dataset_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetRuns - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.get_runs( - dataset_name="datasetName", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedDatasetRuns, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncDatasetsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasets: - """ - Get all datasets - - Parameters - ---------- - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasets - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/v2/datasets", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedDatasets, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get( - self, - dataset_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> Dataset: - """ - Get a dataset - - Parameters - ---------- - dataset_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Dataset - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.get( - dataset_name="datasetName", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/v2/datasets/{jsonable_encoder(dataset_name)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Dataset, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, - *, - request: CreateDatasetRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> Dataset: - """ - Create a dataset - - Parameters - ---------- - request : CreateDatasetRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Dataset - - Examples - -------- - import asyncio - - from langfuse import CreateDatasetRequest - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.create( - request=CreateDatasetRequest( - name="name", - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/v2/datasets", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Dataset, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetRunWithItems: - """ - Get a dataset run and its items - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetRunWithItems - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.get_run( - dataset_name="datasetName", - run_name="runName", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DatasetRunWithItems, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DeleteDatasetRunResponse: - """ - Delete a dataset run and all its run items. This action is irreversible. - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteDatasetRunResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.delete_run( - dataset_name="datasetName", - run_name="runName", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - DeleteDatasetRunResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_runs( - self, - dataset_name: str, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetRuns: - """ - Get dataset runs - - Parameters - ---------- - dataset_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetRuns - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.get_runs( - dataset_name="datasetName", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedDatasetRuns, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/datasets/types/__init__.py b/langfuse/api/resources/datasets/types/__init__.py deleted file mode 100644 index f3304a59f..000000000 --- a/langfuse/api/resources/datasets/types/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_dataset_request import CreateDatasetRequest -from .delete_dataset_run_response import DeleteDatasetRunResponse -from .paginated_dataset_runs import PaginatedDatasetRuns -from .paginated_datasets import PaginatedDatasets - -__all__ = [ - "CreateDatasetRequest", - "DeleteDatasetRunResponse", - "PaginatedDatasetRuns", - "PaginatedDatasets", -] diff --git a/langfuse/api/resources/datasets/types/create_dataset_request.py b/langfuse/api/resources/datasets/types/create_dataset_request.py deleted file mode 100644 index 023cb4c12..000000000 --- a/langfuse/api/resources/datasets/types/create_dataset_request.py +++ /dev/null @@ -1,44 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class CreateDatasetRequest(pydantic_v1.BaseModel): - name: str - description: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/datasets/types/delete_dataset_run_response.py b/langfuse/api/resources/datasets/types/delete_dataset_run_response.py deleted file mode 100644 index cf52eca14..000000000 --- a/langfuse/api/resources/datasets/types/delete_dataset_run_response.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class DeleteDatasetRunResponse(pydantic_v1.BaseModel): - message: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/datasets/types/paginated_dataset_runs.py b/langfuse/api/resources/datasets/types/paginated_dataset_runs.py deleted file mode 100644 index 86f2f0a73..000000000 --- a/langfuse/api/resources/datasets/types/paginated_dataset_runs.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.dataset_run import DatasetRun -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class PaginatedDatasetRuns(pydantic_v1.BaseModel): - data: typing.List[DatasetRun] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/datasets/types/paginated_datasets.py b/langfuse/api/resources/datasets/types/paginated_datasets.py deleted file mode 100644 index c2d436bf4..000000000 --- a/langfuse/api/resources/datasets/types/paginated_datasets.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.dataset import Dataset -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class PaginatedDatasets(pydantic_v1.BaseModel): - data: typing.List[Dataset] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/health/__init__.py b/langfuse/api/resources/health/__init__.py deleted file mode 100644 index f468cdffb..000000000 --- a/langfuse/api/resources/health/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import HealthResponse -from .errors import ServiceUnavailableError - -__all__ = ["HealthResponse", "ServiceUnavailableError"] diff --git a/langfuse/api/resources/health/client.py b/langfuse/api/resources/health/client.py deleted file mode 100644 index 029be7a0c..000000000 --- a/langfuse/api/resources/health/client.py +++ /dev/null @@ -1,154 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from .errors.service_unavailable_error import ServiceUnavailableError -from .types.health_response import HealthResponse - - -class HealthClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def health( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HealthResponse: - """ - Check health of API and database - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HealthResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.health.health() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/health", method="GET", request_options=request_options - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(HealthResponse, _response.json()) # type: ignore - if _response.status_code == 503: - raise ServiceUnavailableError() - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncHealthClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def health( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HealthResponse: - """ - Check health of API and database - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HealthResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.health.health() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/health", method="GET", request_options=request_options - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(HealthResponse, _response.json()) # type: ignore - if _response.status_code == 503: - raise ServiceUnavailableError() - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/health/errors/__init__.py b/langfuse/api/resources/health/errors/__init__.py deleted file mode 100644 index 46bb3fedd..000000000 --- a/langfuse/api/resources/health/errors/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .service_unavailable_error import ServiceUnavailableError - -__all__ = ["ServiceUnavailableError"] diff --git a/langfuse/api/resources/health/errors/service_unavailable_error.py b/langfuse/api/resources/health/errors/service_unavailable_error.py deleted file mode 100644 index acfd8fbf3..000000000 --- a/langfuse/api/resources/health/errors/service_unavailable_error.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ....core.api_error import ApiError - - -class ServiceUnavailableError(ApiError): - def __init__(self) -> None: - super().__init__(status_code=503) diff --git a/langfuse/api/resources/health/types/__init__.py b/langfuse/api/resources/health/types/__init__.py deleted file mode 100644 index 5fb7ec574..000000000 --- a/langfuse/api/resources/health/types/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .health_response import HealthResponse - -__all__ = ["HealthResponse"] diff --git a/langfuse/api/resources/health/types/health_response.py b/langfuse/api/resources/health/types/health_response.py deleted file mode 100644 index 633da67a8..000000000 --- a/langfuse/api/resources/health/types/health_response.py +++ /dev/null @@ -1,58 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class HealthResponse(pydantic_v1.BaseModel): - """ - Examples - -------- - from langfuse import HealthResponse - - HealthResponse( - version="1.25.0", - status="OK", - ) - """ - - version: str = pydantic_v1.Field() - """ - Langfuse server version - """ - - status: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/__init__.py b/langfuse/api/resources/ingestion/__init__.py deleted file mode 100644 index 9e072dc17..000000000 --- a/langfuse/api/resources/ingestion/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - BaseEvent, - CreateEventBody, - CreateEventEvent, - CreateGenerationBody, - CreateGenerationEvent, - CreateObservationEvent, - CreateSpanBody, - CreateSpanEvent, - IngestionError, - IngestionEvent, - IngestionEvent_EventCreate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, - IngestionEvent_ScoreCreate, - IngestionEvent_SdkLog, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_TraceCreate, - IngestionResponse, - IngestionSuccess, - IngestionUsage, - ObservationBody, - ObservationType, - OpenAiCompletionUsageSchema, - OpenAiResponseUsageSchema, - OpenAiUsage, - OptionalObservationBody, - ScoreBody, - ScoreEvent, - SdkLogBody, - SdkLogEvent, - TraceBody, - TraceEvent, - UpdateEventBody, - UpdateGenerationBody, - UpdateGenerationEvent, - UpdateObservationEvent, - UpdateSpanBody, - UpdateSpanEvent, - UsageDetails, -) - -__all__ = [ - "BaseEvent", - "CreateEventBody", - "CreateEventEvent", - "CreateGenerationBody", - "CreateGenerationEvent", - "CreateObservationEvent", - "CreateSpanBody", - "CreateSpanEvent", - "IngestionError", - "IngestionEvent", - "IngestionEvent_EventCreate", - "IngestionEvent_GenerationCreate", - "IngestionEvent_GenerationUpdate", - "IngestionEvent_ObservationCreate", - "IngestionEvent_ObservationUpdate", - "IngestionEvent_ScoreCreate", - "IngestionEvent_SdkLog", - "IngestionEvent_SpanCreate", - "IngestionEvent_SpanUpdate", - "IngestionEvent_TraceCreate", - "IngestionResponse", - "IngestionSuccess", - "IngestionUsage", - "ObservationBody", - "ObservationType", - "OpenAiCompletionUsageSchema", - "OpenAiResponseUsageSchema", - "OpenAiUsage", - "OptionalObservationBody", - "ScoreBody", - "ScoreEvent", - "SdkLogBody", - "SdkLogEvent", - "TraceBody", - "TraceEvent", - "UpdateEventBody", - "UpdateGenerationBody", - "UpdateGenerationEvent", - "UpdateObservationEvent", - "UpdateSpanBody", - "UpdateSpanEvent", - "UsageDetails", -] diff --git a/langfuse/api/resources/ingestion/types/__init__.py b/langfuse/api/resources/ingestion/types/__init__.py deleted file mode 100644 index a3490e4dc..000000000 --- a/langfuse/api/resources/ingestion/types/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .base_event import BaseEvent -from .create_event_body import CreateEventBody -from .create_event_event import CreateEventEvent -from .create_generation_body import CreateGenerationBody -from .create_generation_event import CreateGenerationEvent -from .create_observation_event import CreateObservationEvent -from .create_span_body import CreateSpanBody -from .create_span_event import CreateSpanEvent -from .ingestion_error import IngestionError -from .ingestion_event import ( - IngestionEvent, - IngestionEvent_EventCreate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, - IngestionEvent_ScoreCreate, - IngestionEvent_SdkLog, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_TraceCreate, -) -from .ingestion_response import IngestionResponse -from .ingestion_success import IngestionSuccess -from .ingestion_usage import IngestionUsage -from .observation_body import ObservationBody -from .observation_type import ObservationType -from .open_ai_completion_usage_schema import OpenAiCompletionUsageSchema -from .open_ai_response_usage_schema import OpenAiResponseUsageSchema -from .open_ai_usage import OpenAiUsage -from .optional_observation_body import OptionalObservationBody -from .score_body import ScoreBody -from .score_event import ScoreEvent -from .sdk_log_body import SdkLogBody -from .sdk_log_event import SdkLogEvent -from .trace_body import TraceBody -from .trace_event import TraceEvent -from .update_event_body import UpdateEventBody -from .update_generation_body import UpdateGenerationBody -from .update_generation_event import UpdateGenerationEvent -from .update_observation_event import UpdateObservationEvent -from .update_span_body import UpdateSpanBody -from .update_span_event import UpdateSpanEvent -from .usage_details import UsageDetails - -__all__ = [ - "BaseEvent", - "CreateEventBody", - "CreateEventEvent", - "CreateGenerationBody", - "CreateGenerationEvent", - "CreateObservationEvent", - "CreateSpanBody", - "CreateSpanEvent", - "IngestionError", - "IngestionEvent", - "IngestionEvent_EventCreate", - "IngestionEvent_GenerationCreate", - "IngestionEvent_GenerationUpdate", - "IngestionEvent_ObservationCreate", - "IngestionEvent_ObservationUpdate", - "IngestionEvent_ScoreCreate", - "IngestionEvent_SdkLog", - "IngestionEvent_SpanCreate", - "IngestionEvent_SpanUpdate", - "IngestionEvent_TraceCreate", - "IngestionResponse", - "IngestionSuccess", - "IngestionUsage", - "ObservationBody", - "ObservationType", - "OpenAiCompletionUsageSchema", - "OpenAiResponseUsageSchema", - "OpenAiUsage", - "OptionalObservationBody", - "ScoreBody", - "ScoreEvent", - "SdkLogBody", - "SdkLogEvent", - "TraceBody", - "TraceEvent", - "UpdateEventBody", - "UpdateGenerationBody", - "UpdateGenerationEvent", - "UpdateObservationEvent", - "UpdateSpanBody", - "UpdateSpanEvent", - "UsageDetails", -] diff --git a/langfuse/api/resources/ingestion/types/base_event.py b/langfuse/api/resources/ingestion/types/base_event.py deleted file mode 100644 index dec8a52e7..000000000 --- a/langfuse/api/resources/ingestion/types/base_event.py +++ /dev/null @@ -1,55 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class BaseEvent(pydantic_v1.BaseModel): - id: str = pydantic_v1.Field() - """ - UUID v4 that identifies the event - """ - - timestamp: str = pydantic_v1.Field() - """ - Datetime (ISO 8601) of event creation in client. Should be as close to actual event creation in client as possible, this timestamp will be used for ordering of events in future release. Resolution: milliseconds (required), microseconds (optimal). - """ - - metadata: typing.Optional[typing.Any] = pydantic_v1.Field(default=None) - """ - Optional. Metadata field used by the Langfuse SDKs for debugging. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/create_event_body.py b/langfuse/api/resources/ingestion/types/create_event_body.py deleted file mode 100644 index afe8677f3..000000000 --- a/langfuse/api/resources/ingestion/types/create_event_body.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .optional_observation_body import OptionalObservationBody - - -class CreateEventBody(OptionalObservationBody): - id: typing.Optional[str] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/create_event_event.py b/langfuse/api/resources/ingestion/types/create_event_event.py deleted file mode 100644 index 0c3cce040..000000000 --- a/langfuse/api/resources/ingestion/types/create_event_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .create_event_body import CreateEventBody - - -class CreateEventEvent(BaseEvent): - body: CreateEventBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/create_generation_body.py b/langfuse/api/resources/ingestion/types/create_generation_body.py deleted file mode 100644 index 428b58607..000000000 --- a/langfuse/api/resources/ingestion/types/create_generation_body.py +++ /dev/null @@ -1,67 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.map_value import MapValue -from .create_span_body import CreateSpanBody -from .ingestion_usage import IngestionUsage -from .usage_details import UsageDetails - - -class CreateGenerationBody(CreateSpanBody): - completion_start_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="completionStartTime", default=None - ) - model: typing.Optional[str] = None - model_parameters: typing.Optional[typing.Dict[str, MapValue]] = pydantic_v1.Field( - alias="modelParameters", default=None - ) - usage: typing.Optional[IngestionUsage] = None - usage_details: typing.Optional[UsageDetails] = pydantic_v1.Field( - alias="usageDetails", default=None - ) - cost_details: typing.Optional[typing.Dict[str, float]] = pydantic_v1.Field( - alias="costDetails", default=None - ) - prompt_name: typing.Optional[str] = pydantic_v1.Field( - alias="promptName", default=None - ) - prompt_version: typing.Optional[int] = pydantic_v1.Field( - alias="promptVersion", default=None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/create_generation_event.py b/langfuse/api/resources/ingestion/types/create_generation_event.py deleted file mode 100644 index cb7b484dd..000000000 --- a/langfuse/api/resources/ingestion/types/create_generation_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .create_generation_body import CreateGenerationBody - - -class CreateGenerationEvent(BaseEvent): - body: CreateGenerationBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/create_observation_event.py b/langfuse/api/resources/ingestion/types/create_observation_event.py deleted file mode 100644 index adfefc793..000000000 --- a/langfuse/api/resources/ingestion/types/create_observation_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .observation_body import ObservationBody - - -class CreateObservationEvent(BaseEvent): - body: ObservationBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/create_span_body.py b/langfuse/api/resources/ingestion/types/create_span_body.py deleted file mode 100644 index c31fde567..000000000 --- a/langfuse/api/resources/ingestion/types/create_span_body.py +++ /dev/null @@ -1,47 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .create_event_body import CreateEventBody - - -class CreateSpanBody(CreateEventBody): - end_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="endTime", default=None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/create_span_event.py b/langfuse/api/resources/ingestion/types/create_span_event.py deleted file mode 100644 index 7a8e8154c..000000000 --- a/langfuse/api/resources/ingestion/types/create_span_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .create_span_body import CreateSpanBody - - -class CreateSpanEvent(BaseEvent): - body: CreateSpanBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/ingestion_error.py b/langfuse/api/resources/ingestion/types/ingestion_error.py deleted file mode 100644 index b9028ce1d..000000000 --- a/langfuse/api/resources/ingestion/types/ingestion_error.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class IngestionError(pydantic_v1.BaseModel): - id: str - status: int - message: typing.Optional[str] = None - error: typing.Optional[typing.Any] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/ingestion_event.py b/langfuse/api/resources/ingestion/types/ingestion_event.py deleted file mode 100644 index e083c9354..000000000 --- a/langfuse/api/resources/ingestion/types/ingestion_event.py +++ /dev/null @@ -1,422 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .create_event_body import CreateEventBody -from .create_generation_body import CreateGenerationBody -from .create_span_body import CreateSpanBody -from .observation_body import ObservationBody -from .score_body import ScoreBody -from .sdk_log_body import SdkLogBody -from .trace_body import TraceBody -from .update_generation_body import UpdateGenerationBody -from .update_span_body import UpdateSpanBody - - -class IngestionEvent_TraceCreate(pydantic_v1.BaseModel): - body: TraceBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["trace-create"] = "trace-create" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_ScoreCreate(pydantic_v1.BaseModel): - body: ScoreBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["score-create"] = "score-create" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_SpanCreate(pydantic_v1.BaseModel): - body: CreateSpanBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["span-create"] = "span-create" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_SpanUpdate(pydantic_v1.BaseModel): - body: UpdateSpanBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["span-update"] = "span-update" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_GenerationCreate(pydantic_v1.BaseModel): - body: CreateGenerationBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["generation-create"] = "generation-create" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_GenerationUpdate(pydantic_v1.BaseModel): - body: UpdateGenerationBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["generation-update"] = "generation-update" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_EventCreate(pydantic_v1.BaseModel): - body: CreateEventBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["event-create"] = "event-create" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_SdkLog(pydantic_v1.BaseModel): - body: SdkLogBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["sdk-log"] = "sdk-log" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_ObservationCreate(pydantic_v1.BaseModel): - body: ObservationBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["observation-create"] = "observation-create" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class IngestionEvent_ObservationUpdate(pydantic_v1.BaseModel): - body: ObservationBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - type: typing.Literal["observation-update"] = "observation-update" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -IngestionEvent = typing.Union[ - IngestionEvent_TraceCreate, - IngestionEvent_ScoreCreate, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_EventCreate, - IngestionEvent_SdkLog, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, -] diff --git a/langfuse/api/resources/ingestion/types/ingestion_response.py b/langfuse/api/resources/ingestion/types/ingestion_response.py deleted file mode 100644 index b4e66349c..000000000 --- a/langfuse/api/resources/ingestion/types/ingestion_response.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .ingestion_error import IngestionError -from .ingestion_success import IngestionSuccess - - -class IngestionResponse(pydantic_v1.BaseModel): - successes: typing.List[IngestionSuccess] - errors: typing.List[IngestionError] - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/ingestion_success.py b/langfuse/api/resources/ingestion/types/ingestion_success.py deleted file mode 100644 index 481e64752..000000000 --- a/langfuse/api/resources/ingestion/types/ingestion_success.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class IngestionSuccess(pydantic_v1.BaseModel): - id: str - status: int - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/observation_body.py b/langfuse/api/resources/ingestion/types/observation_body.py deleted file mode 100644 index d191a1f12..000000000 --- a/langfuse/api/resources/ingestion/types/observation_body.py +++ /dev/null @@ -1,77 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.map_value import MapValue -from ...commons.types.observation_level import ObservationLevel -from ...commons.types.usage import Usage -from .observation_type import ObservationType - - -class ObservationBody(pydantic_v1.BaseModel): - id: typing.Optional[str] = None - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - type: ObservationType - name: typing.Optional[str] = None - start_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="startTime", default=None - ) - end_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="endTime", default=None - ) - completion_start_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="completionStartTime", default=None - ) - model: typing.Optional[str] = None - model_parameters: typing.Optional[typing.Dict[str, MapValue]] = pydantic_v1.Field( - alias="modelParameters", default=None - ) - input: typing.Optional[typing.Any] = None - version: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - output: typing.Optional[typing.Any] = None - usage: typing.Optional[Usage] = None - level: typing.Optional[ObservationLevel] = None - status_message: typing.Optional[str] = pydantic_v1.Field( - alias="statusMessage", default=None - ) - parent_observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="parentObservationId", default=None - ) - environment: typing.Optional[str] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/observation_type.py b/langfuse/api/resources/ingestion/types/observation_type.py deleted file mode 100644 index 0af377c3c..000000000 --- a/langfuse/api/resources/ingestion/types/observation_type.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import enum -import typing - -T_Result = typing.TypeVar("T_Result") - - -class ObservationType(str, enum.Enum): - SPAN = "SPAN" - GENERATION = "GENERATION" - EVENT = "EVENT" - - def visit( - self, - span: typing.Callable[[], T_Result], - generation: typing.Callable[[], T_Result], - event: typing.Callable[[], T_Result], - ) -> T_Result: - if self is ObservationType.SPAN: - return span() - if self is ObservationType.GENERATION: - return generation() - if self is ObservationType.EVENT: - return event() diff --git a/langfuse/api/resources/ingestion/types/open_ai_completion_usage_schema.py b/langfuse/api/resources/ingestion/types/open_ai_completion_usage_schema.py deleted file mode 100644 index 368a7da03..000000000 --- a/langfuse/api/resources/ingestion/types/open_ai_completion_usage_schema.py +++ /dev/null @@ -1,54 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class OpenAiCompletionUsageSchema(pydantic_v1.BaseModel): - """ - OpenAI Usage schema from (Chat-)Completion APIs - """ - - prompt_tokens: int - completion_tokens: int - total_tokens: int - prompt_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = ( - None - ) - completion_tokens_details: typing.Optional[ - typing.Dict[str, typing.Optional[int]] - ] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/open_ai_response_usage_schema.py b/langfuse/api/resources/ingestion/types/open_ai_response_usage_schema.py deleted file mode 100644 index 0c68e6a7d..000000000 --- a/langfuse/api/resources/ingestion/types/open_ai_response_usage_schema.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class OpenAiResponseUsageSchema(pydantic_v1.BaseModel): - """ - OpenAI Usage schema from Response API - """ - - input_tokens: int - output_tokens: int - total_tokens: int - input_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = None - output_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = ( - None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/open_ai_usage.py b/langfuse/api/resources/ingestion/types/open_ai_usage.py deleted file mode 100644 index 86e7ebd82..000000000 --- a/langfuse/api/resources/ingestion/types/open_ai_usage.py +++ /dev/null @@ -1,56 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class OpenAiUsage(pydantic_v1.BaseModel): - """ - Usage interface of OpenAI for improved compatibility. - """ - - prompt_tokens: typing.Optional[int] = pydantic_v1.Field( - alias="promptTokens", default=None - ) - completion_tokens: typing.Optional[int] = pydantic_v1.Field( - alias="completionTokens", default=None - ) - total_tokens: typing.Optional[int] = pydantic_v1.Field( - alias="totalTokens", default=None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/optional_observation_body.py b/langfuse/api/resources/ingestion/types/optional_observation_body.py deleted file mode 100644 index 7302d30f9..000000000 --- a/langfuse/api/resources/ingestion/types/optional_observation_body.py +++ /dev/null @@ -1,61 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.observation_level import ObservationLevel - - -class OptionalObservationBody(pydantic_v1.BaseModel): - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - name: typing.Optional[str] = None - start_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="startTime", default=None - ) - metadata: typing.Optional[typing.Any] = None - input: typing.Optional[typing.Any] = None - output: typing.Optional[typing.Any] = None - level: typing.Optional[ObservationLevel] = None - status_message: typing.Optional[str] = pydantic_v1.Field( - alias="statusMessage", default=None - ) - parent_observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="parentObservationId", default=None - ) - version: typing.Optional[str] = None - environment: typing.Optional[str] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/score_body.py b/langfuse/api/resources/ingestion/types/score_body.py deleted file mode 100644 index 286c06514..000000000 --- a/langfuse/api/resources/ingestion/types/score_body.py +++ /dev/null @@ -1,88 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.create_score_value import CreateScoreValue -from ...commons.types.score_data_type import ScoreDataType - - -class ScoreBody(pydantic_v1.BaseModel): - """ - Examples - -------- - from langfuse import ScoreBody - - ScoreBody( - name="novelty", - value=0.9, - trace_id="cdef-1234-5678-90ab", - ) - """ - - id: typing.Optional[str] = None - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - environment: typing.Optional[str] = None - value: CreateScoreValue = pydantic_v1.Field() - """ - The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false) - """ - - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - data_type: typing.Optional[ScoreDataType] = pydantic_v1.Field( - alias="dataType", default=None - ) - """ - When set, must match the score value's type. If not set, will be inferred from the score value or config - """ - - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - """ - Reference a score config on a score. When set, the score name must equal the config name and scores must comply with the config's range and data type. For categorical scores, the value must map to a config category. Numeric scores might be constrained by the score config's max and min values - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/score_event.py b/langfuse/api/resources/ingestion/types/score_event.py deleted file mode 100644 index ea05aedef..000000000 --- a/langfuse/api/resources/ingestion/types/score_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .score_body import ScoreBody - - -class ScoreEvent(BaseEvent): - body: ScoreBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/sdk_log_body.py b/langfuse/api/resources/ingestion/types/sdk_log_body.py deleted file mode 100644 index df8972860..000000000 --- a/langfuse/api/resources/ingestion/types/sdk_log_body.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class SdkLogBody(pydantic_v1.BaseModel): - log: typing.Any - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/sdk_log_event.py b/langfuse/api/resources/ingestion/types/sdk_log_event.py deleted file mode 100644 index d7ad87de8..000000000 --- a/langfuse/api/resources/ingestion/types/sdk_log_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .sdk_log_body import SdkLogBody - - -class SdkLogEvent(BaseEvent): - body: SdkLogBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/trace_body.py b/langfuse/api/resources/ingestion/types/trace_body.py deleted file mode 100644 index 3f5550435..000000000 --- a/langfuse/api/resources/ingestion/types/trace_body.py +++ /dev/null @@ -1,61 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class TraceBody(pydantic_v1.BaseModel): - id: typing.Optional[str] = None - timestamp: typing.Optional[dt.datetime] = None - name: typing.Optional[str] = None - user_id: typing.Optional[str] = pydantic_v1.Field(alias="userId", default=None) - input: typing.Optional[typing.Any] = None - output: typing.Optional[typing.Any] = None - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - release: typing.Optional[str] = None - version: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - tags: typing.Optional[typing.List[str]] = None - environment: typing.Optional[str] = None - public: typing.Optional[bool] = pydantic_v1.Field(default=None) - """ - Make trace publicly accessible via url - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/trace_event.py b/langfuse/api/resources/ingestion/types/trace_event.py deleted file mode 100644 index b84ddd615..000000000 --- a/langfuse/api/resources/ingestion/types/trace_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .trace_body import TraceBody - - -class TraceEvent(BaseEvent): - body: TraceBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/update_event_body.py b/langfuse/api/resources/ingestion/types/update_event_body.py deleted file mode 100644 index 35bbb359b..000000000 --- a/langfuse/api/resources/ingestion/types/update_event_body.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .optional_observation_body import OptionalObservationBody - - -class UpdateEventBody(OptionalObservationBody): - id: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/update_generation_body.py b/langfuse/api/resources/ingestion/types/update_generation_body.py deleted file mode 100644 index 2058543af..000000000 --- a/langfuse/api/resources/ingestion/types/update_generation_body.py +++ /dev/null @@ -1,67 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.map_value import MapValue -from .ingestion_usage import IngestionUsage -from .update_span_body import UpdateSpanBody -from .usage_details import UsageDetails - - -class UpdateGenerationBody(UpdateSpanBody): - completion_start_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="completionStartTime", default=None - ) - model: typing.Optional[str] = None - model_parameters: typing.Optional[typing.Dict[str, MapValue]] = pydantic_v1.Field( - alias="modelParameters", default=None - ) - usage: typing.Optional[IngestionUsage] = None - prompt_name: typing.Optional[str] = pydantic_v1.Field( - alias="promptName", default=None - ) - usage_details: typing.Optional[UsageDetails] = pydantic_v1.Field( - alias="usageDetails", default=None - ) - cost_details: typing.Optional[typing.Dict[str, float]] = pydantic_v1.Field( - alias="costDetails", default=None - ) - prompt_version: typing.Optional[int] = pydantic_v1.Field( - alias="promptVersion", default=None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/update_generation_event.py b/langfuse/api/resources/ingestion/types/update_generation_event.py deleted file mode 100644 index da8f6a9fa..000000000 --- a/langfuse/api/resources/ingestion/types/update_generation_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .update_generation_body import UpdateGenerationBody - - -class UpdateGenerationEvent(BaseEvent): - body: UpdateGenerationBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/update_observation_event.py b/langfuse/api/resources/ingestion/types/update_observation_event.py deleted file mode 100644 index 9d7af357f..000000000 --- a/langfuse/api/resources/ingestion/types/update_observation_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .observation_body import ObservationBody - - -class UpdateObservationEvent(BaseEvent): - body: ObservationBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/update_span_body.py b/langfuse/api/resources/ingestion/types/update_span_body.py deleted file mode 100644 index e3484879b..000000000 --- a/langfuse/api/resources/ingestion/types/update_span_body.py +++ /dev/null @@ -1,47 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .update_event_body import UpdateEventBody - - -class UpdateSpanBody(UpdateEventBody): - end_time: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="endTime", default=None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/ingestion/types/update_span_event.py b/langfuse/api/resources/ingestion/types/update_span_event.py deleted file mode 100644 index ec7d83b15..000000000 --- a/langfuse/api/resources/ingestion/types/update_span_event.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_event import BaseEvent -from .update_span_body import UpdateSpanBody - - -class UpdateSpanEvent(BaseEvent): - body: UpdateSpanBody - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/media/__init__.py b/langfuse/api/resources/media/__init__.py deleted file mode 100644 index f337d7a04..000000000 --- a/langfuse/api/resources/media/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - GetMediaResponse, - GetMediaUploadUrlRequest, - GetMediaUploadUrlResponse, - MediaContentType, - PatchMediaBody, -) - -__all__ = [ - "GetMediaResponse", - "GetMediaUploadUrlRequest", - "GetMediaUploadUrlResponse", - "MediaContentType", - "PatchMediaBody", -] diff --git a/langfuse/api/resources/media/client.py b/langfuse/api/resources/media/client.py deleted file mode 100644 index bb8e4b149..000000000 --- a/langfuse/api/resources/media/client.py +++ /dev/null @@ -1,505 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from .types.get_media_response import GetMediaResponse -from .types.get_media_upload_url_request import GetMediaUploadUrlRequest -from .types.get_media_upload_url_response import GetMediaUploadUrlResponse -from .types.patch_media_body import PatchMediaBody - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class MediaClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get( - self, media_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> GetMediaResponse: - """ - Get a media record - - Parameters - ---------- - media_id : str - The unique langfuse identifier of a media record - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetMediaResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.media.get( - media_id="mediaId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/media/{jsonable_encoder(media_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(GetMediaResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def patch( - self, - media_id: str, - *, - request: PatchMediaBody, - request_options: typing.Optional[RequestOptions] = None, - ) -> None: - """ - Patch a media record - - Parameters - ---------- - media_id : str - The unique langfuse identifier of a media record - - request : PatchMediaBody - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import datetime - - from langfuse import PatchMediaBody - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.media.patch( - media_id="mediaId", - request=PatchMediaBody( - uploaded_at=datetime.datetime.fromisoformat( - "2024-01-15 09:30:00+00:00", - ), - upload_http_status=1, - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/media/{jsonable_encoder(media_id)}", - method="PATCH", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_upload_url( - self, - *, - request: GetMediaUploadUrlRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> GetMediaUploadUrlResponse: - """ - Get a presigned upload URL for a media record - - Parameters - ---------- - request : GetMediaUploadUrlRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetMediaUploadUrlResponse - - Examples - -------- - from langfuse import GetMediaUploadUrlRequest, MediaContentType - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.media.get_upload_url( - request=GetMediaUploadUrlRequest( - trace_id="traceId", - content_type=MediaContentType.IMAGE_PNG, - content_length=1, - sha_256_hash="sha256Hash", - field="field", - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/media", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - GetMediaUploadUrlResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncMediaClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get( - self, media_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> GetMediaResponse: - """ - Get a media record - - Parameters - ---------- - media_id : str - The unique langfuse identifier of a media record - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetMediaResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.media.get( - media_id="mediaId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/media/{jsonable_encoder(media_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(GetMediaResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def patch( - self, - media_id: str, - *, - request: PatchMediaBody, - request_options: typing.Optional[RequestOptions] = None, - ) -> None: - """ - Patch a media record - - Parameters - ---------- - media_id : str - The unique langfuse identifier of a media record - - request : PatchMediaBody - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - import datetime - - from langfuse import PatchMediaBody - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.media.patch( - media_id="mediaId", - request=PatchMediaBody( - uploaded_at=datetime.datetime.fromisoformat( - "2024-01-15 09:30:00+00:00", - ), - upload_http_status=1, - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/media/{jsonable_encoder(media_id)}", - method="PATCH", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_upload_url( - self, - *, - request: GetMediaUploadUrlRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> GetMediaUploadUrlResponse: - """ - Get a presigned upload URL for a media record - - Parameters - ---------- - request : GetMediaUploadUrlRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetMediaUploadUrlResponse - - Examples - -------- - import asyncio - - from langfuse import GetMediaUploadUrlRequest, MediaContentType - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.media.get_upload_url( - request=GetMediaUploadUrlRequest( - trace_id="traceId", - content_type=MediaContentType.IMAGE_PNG, - content_length=1, - sha_256_hash="sha256Hash", - field="field", - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/media", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - GetMediaUploadUrlResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/media/types/__init__.py b/langfuse/api/resources/media/types/__init__.py deleted file mode 100644 index 20af676d8..000000000 --- a/langfuse/api/resources/media/types/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .get_media_response import GetMediaResponse -from .get_media_upload_url_request import GetMediaUploadUrlRequest -from .get_media_upload_url_response import GetMediaUploadUrlResponse -from .media_content_type import MediaContentType -from .patch_media_body import PatchMediaBody - -__all__ = [ - "GetMediaResponse", - "GetMediaUploadUrlRequest", - "GetMediaUploadUrlResponse", - "MediaContentType", - "PatchMediaBody", -] diff --git a/langfuse/api/resources/media/types/get_media_response.py b/langfuse/api/resources/media/types/get_media_response.py deleted file mode 100644 index fa5368872..000000000 --- a/langfuse/api/resources/media/types/get_media_response.py +++ /dev/null @@ -1,72 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class GetMediaResponse(pydantic_v1.BaseModel): - media_id: str = pydantic_v1.Field(alias="mediaId") - """ - The unique langfuse identifier of a media record - """ - - content_type: str = pydantic_v1.Field(alias="contentType") - """ - The MIME type of the media record - """ - - content_length: int = pydantic_v1.Field(alias="contentLength") - """ - The size of the media record in bytes - """ - - uploaded_at: dt.datetime = pydantic_v1.Field(alias="uploadedAt") - """ - The date and time when the media record was uploaded - """ - - url: str = pydantic_v1.Field() - """ - The download URL of the media record - """ - - url_expiry: str = pydantic_v1.Field(alias="urlExpiry") - """ - The expiry date and time of the media record download URL - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/media/types/get_media_upload_url_request.py b/langfuse/api/resources/media/types/get_media_upload_url_request.py deleted file mode 100644 index d0cde59fe..000000000 --- a/langfuse/api/resources/media/types/get_media_upload_url_request.py +++ /dev/null @@ -1,71 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .media_content_type import MediaContentType - - -class GetMediaUploadUrlRequest(pydantic_v1.BaseModel): - trace_id: str = pydantic_v1.Field(alias="traceId") - """ - The trace ID associated with the media record - """ - - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - """ - The observation ID associated with the media record. If the media record is associated directly with a trace, this will be null. - """ - - content_type: MediaContentType = pydantic_v1.Field(alias="contentType") - content_length: int = pydantic_v1.Field(alias="contentLength") - """ - The size of the media record in bytes - """ - - sha_256_hash: str = pydantic_v1.Field(alias="sha256Hash") - """ - The SHA-256 hash of the media record - """ - - field: str = pydantic_v1.Field() - """ - The trace / observation field the media record is associated with. This can be one of `input`, `output`, `metadata` - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/media/types/get_media_upload_url_response.py b/langfuse/api/resources/media/types/get_media_upload_url_response.py deleted file mode 100644 index fadc76c01..000000000 --- a/langfuse/api/resources/media/types/get_media_upload_url_response.py +++ /dev/null @@ -1,54 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class GetMediaUploadUrlResponse(pydantic_v1.BaseModel): - upload_url: typing.Optional[str] = pydantic_v1.Field( - alias="uploadUrl", default=None - ) - """ - The presigned upload URL. If the asset is already uploaded, this will be null - """ - - media_id: str = pydantic_v1.Field(alias="mediaId") - """ - The unique langfuse identifier of a media record - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/media/types/media_content_type.py b/langfuse/api/resources/media/types/media_content_type.py deleted file mode 100644 index e8fdeefa2..000000000 --- a/langfuse/api/resources/media/types/media_content_type.py +++ /dev/null @@ -1,133 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import enum -import typing - -T_Result = typing.TypeVar("T_Result") - - -class MediaContentType(str, enum.Enum): - """ - The MIME type of the media record - """ - - IMAGE_PNG = "image/png" - IMAGE_JPEG = "image/jpeg" - IMAGE_JPG = "image/jpg" - IMAGE_WEBP = "image/webp" - IMAGE_GIF = "image/gif" - IMAGE_SVG_XML = "image/svg+xml" - IMAGE_TIFF = "image/tiff" - IMAGE_BMP = "image/bmp" - AUDIO_MPEG = "audio/mpeg" - AUDIO_MP_3 = "audio/mp3" - AUDIO_WAV = "audio/wav" - AUDIO_OGG = "audio/ogg" - AUDIO_OGA = "audio/oga" - AUDIO_AAC = "audio/aac" - AUDIO_MP_4 = "audio/mp4" - AUDIO_FLAC = "audio/flac" - VIDEO_MP_4 = "video/mp4" - VIDEO_WEBM = "video/webm" - TEXT_PLAIN = "text/plain" - TEXT_HTML = "text/html" - TEXT_CSS = "text/css" - TEXT_CSV = "text/csv" - APPLICATION_PDF = "application/pdf" - APPLICATION_MSWORD = "application/msword" - APPLICATION_MS_EXCEL = "application/vnd.ms-excel" - APPLICATION_ZIP = "application/zip" - APPLICATION_JSON = "application/json" - APPLICATION_XML = "application/xml" - APPLICATION_OCTET_STREAM = "application/octet-stream" - - def visit( - self, - image_png: typing.Callable[[], T_Result], - image_jpeg: typing.Callable[[], T_Result], - image_jpg: typing.Callable[[], T_Result], - image_webp: typing.Callable[[], T_Result], - image_gif: typing.Callable[[], T_Result], - image_svg_xml: typing.Callable[[], T_Result], - image_tiff: typing.Callable[[], T_Result], - image_bmp: typing.Callable[[], T_Result], - audio_mpeg: typing.Callable[[], T_Result], - audio_mp_3: typing.Callable[[], T_Result], - audio_wav: typing.Callable[[], T_Result], - audio_ogg: typing.Callable[[], T_Result], - audio_oga: typing.Callable[[], T_Result], - audio_aac: typing.Callable[[], T_Result], - audio_mp_4: typing.Callable[[], T_Result], - audio_flac: typing.Callable[[], T_Result], - video_mp_4: typing.Callable[[], T_Result], - video_webm: typing.Callable[[], T_Result], - text_plain: typing.Callable[[], T_Result], - text_html: typing.Callable[[], T_Result], - text_css: typing.Callable[[], T_Result], - text_csv: typing.Callable[[], T_Result], - application_pdf: typing.Callable[[], T_Result], - application_msword: typing.Callable[[], T_Result], - application_ms_excel: typing.Callable[[], T_Result], - application_zip: typing.Callable[[], T_Result], - application_json: typing.Callable[[], T_Result], - application_xml: typing.Callable[[], T_Result], - application_octet_stream: typing.Callable[[], T_Result], - ) -> T_Result: - if self is MediaContentType.IMAGE_PNG: - return image_png() - if self is MediaContentType.IMAGE_JPEG: - return image_jpeg() - if self is MediaContentType.IMAGE_JPG: - return image_jpg() - if self is MediaContentType.IMAGE_WEBP: - return image_webp() - if self is MediaContentType.IMAGE_GIF: - return image_gif() - if self is MediaContentType.IMAGE_SVG_XML: - return image_svg_xml() - if self is MediaContentType.IMAGE_TIFF: - return image_tiff() - if self is MediaContentType.IMAGE_BMP: - return image_bmp() - if self is MediaContentType.AUDIO_MPEG: - return audio_mpeg() - if self is MediaContentType.AUDIO_MP_3: - return audio_mp_3() - if self is MediaContentType.AUDIO_WAV: - return audio_wav() - if self is MediaContentType.AUDIO_OGG: - return audio_ogg() - if self is MediaContentType.AUDIO_OGA: - return audio_oga() - if self is MediaContentType.AUDIO_AAC: - return audio_aac() - if self is MediaContentType.AUDIO_MP_4: - return audio_mp_4() - if self is MediaContentType.AUDIO_FLAC: - return audio_flac() - if self is MediaContentType.VIDEO_MP_4: - return video_mp_4() - if self is MediaContentType.VIDEO_WEBM: - return video_webm() - if self is MediaContentType.TEXT_PLAIN: - return text_plain() - if self is MediaContentType.TEXT_HTML: - return text_html() - if self is MediaContentType.TEXT_CSS: - return text_css() - if self is MediaContentType.TEXT_CSV: - return text_csv() - if self is MediaContentType.APPLICATION_PDF: - return application_pdf() - if self is MediaContentType.APPLICATION_MSWORD: - return application_msword() - if self is MediaContentType.APPLICATION_MS_EXCEL: - return application_ms_excel() - if self is MediaContentType.APPLICATION_ZIP: - return application_zip() - if self is MediaContentType.APPLICATION_JSON: - return application_json() - if self is MediaContentType.APPLICATION_XML: - return application_xml() - if self is MediaContentType.APPLICATION_OCTET_STREAM: - return application_octet_stream() diff --git a/langfuse/api/resources/media/types/patch_media_body.py b/langfuse/api/resources/media/types/patch_media_body.py deleted file mode 100644 index 49f0c3432..000000000 --- a/langfuse/api/resources/media/types/patch_media_body.py +++ /dev/null @@ -1,66 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class PatchMediaBody(pydantic_v1.BaseModel): - uploaded_at: dt.datetime = pydantic_v1.Field(alias="uploadedAt") - """ - The date and time when the media record was uploaded - """ - - upload_http_status: int = pydantic_v1.Field(alias="uploadHttpStatus") - """ - The HTTP status code of the upload - """ - - upload_http_error: typing.Optional[str] = pydantic_v1.Field( - alias="uploadHttpError", default=None - ) - """ - The HTTP error message of the upload - """ - - upload_time_ms: typing.Optional[int] = pydantic_v1.Field( - alias="uploadTimeMs", default=None - ) - """ - The time in milliseconds it took to upload the media record - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/metrics/__init__.py b/langfuse/api/resources/metrics/__init__.py deleted file mode 100644 index 90e510b5f..000000000 --- a/langfuse/api/resources/metrics/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import MetricsResponse - -__all__ = ["MetricsResponse"] diff --git a/langfuse/api/resources/metrics/types/__init__.py b/langfuse/api/resources/metrics/types/__init__.py deleted file mode 100644 index 7bf03027d..000000000 --- a/langfuse/api/resources/metrics/types/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .metrics_response import MetricsResponse - -__all__ = ["MetricsResponse"] diff --git a/langfuse/api/resources/metrics/types/metrics_response.py b/langfuse/api/resources/metrics/types/metrics_response.py deleted file mode 100644 index af0121c84..000000000 --- a/langfuse/api/resources/metrics/types/metrics_response.py +++ /dev/null @@ -1,47 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class MetricsResponse(pydantic_v1.BaseModel): - data: typing.List[typing.Dict[str, typing.Any]] = pydantic_v1.Field() - """ - The metrics data. Each item in the list contains the metric values and dimensions requested in the query. - Format varies based on the query parameters. - Histograms will return an array with [lower, upper, height] tuples. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/models/__init__.py b/langfuse/api/resources/models/__init__.py deleted file mode 100644 index a41fff3e5..000000000 --- a/langfuse/api/resources/models/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateModelRequest, PaginatedModels - -__all__ = ["CreateModelRequest", "PaginatedModels"] diff --git a/langfuse/api/resources/models/client.py b/langfuse/api/resources/models/client.py deleted file mode 100644 index 4f4b727fa..000000000 --- a/langfuse/api/resources/models/client.py +++ /dev/null @@ -1,607 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.model import Model -from .types.create_model_request import CreateModelRequest -from .types.paginated_models import PaginatedModels - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class ModelsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def create( - self, - *, - request: CreateModelRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> Model: - """ - Create a model - - Parameters - ---------- - request : CreateModelRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Model - - Examples - -------- - from langfuse import CreateModelRequest - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.models.create( - request=CreateModelRequest( - model_name="modelName", - match_pattern="matchPattern", - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/models", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Model, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedModels: - """ - Get all models - - Parameters - ---------- - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedModels - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.models.list() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/models", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedModels, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> Model: - """ - Get a model - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Model - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.models.get( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/models/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Model, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.models.delete( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/models/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncModelsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def create( - self, - *, - request: CreateModelRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> Model: - """ - Create a model - - Parameters - ---------- - request : CreateModelRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Model - - Examples - -------- - import asyncio - - from langfuse import CreateModelRequest - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.models.create( - request=CreateModelRequest( - model_name="modelName", - match_pattern="matchPattern", - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/models", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Model, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedModels: - """ - Get all models - - Parameters - ---------- - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedModels - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.models.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/models", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedModels, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> Model: - """ - Get a model - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Model - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.models.get( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/models/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Model, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.models.delete( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/models/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/models/types/__init__.py b/langfuse/api/resources/models/types/__init__.py deleted file mode 100644 index 94285af35..000000000 --- a/langfuse/api/resources/models/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_model_request import CreateModelRequest -from .paginated_models import PaginatedModels - -__all__ = ["CreateModelRequest", "PaginatedModels"] diff --git a/langfuse/api/resources/models/types/create_model_request.py b/langfuse/api/resources/models/types/create_model_request.py deleted file mode 100644 index b3d8a6462..000000000 --- a/langfuse/api/resources/models/types/create_model_request.py +++ /dev/null @@ -1,100 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.model_usage_unit import ModelUsageUnit - - -class CreateModelRequest(pydantic_v1.BaseModel): - model_name: str = pydantic_v1.Field(alias="modelName") - """ - Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/models/types/paginated_models.py b/langfuse/api/resources/models/types/paginated_models.py deleted file mode 100644 index 3469a1fe6..000000000 --- a/langfuse/api/resources/models/types/paginated_models.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.model import Model -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class PaginatedModels(pydantic_v1.BaseModel): - data: typing.List[Model] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/observations/__init__.py b/langfuse/api/resources/observations/__init__.py deleted file mode 100644 index 95fd7c721..000000000 --- a/langfuse/api/resources/observations/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import Observations, ObservationsViews - -__all__ = ["Observations", "ObservationsViews"] diff --git a/langfuse/api/resources/observations/client.py b/langfuse/api/resources/observations/client.py deleted file mode 100644 index 01bf60f78..000000000 --- a/langfuse/api/resources/observations/client.py +++ /dev/null @@ -1,417 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.datetime_utils import serialize_datetime -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.observations_view import ObservationsView -from .types.observations_views import ObservationsViews - - -class ObservationsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get( - self, - observation_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ObservationsView: - """ - Get a observation - - Parameters - ---------- - observation_id : str - The unique langfuse identifier of an observation, can be an event, span or generation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ObservationsView - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.observations.get( - observation_id="observationId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/observations/{jsonable_encoder(observation_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ObservationsView, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_many( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - name: typing.Optional[str] = None, - user_id: typing.Optional[str] = None, - type: typing.Optional[str] = None, - trace_id: typing.Optional[str] = None, - parent_observation_id: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - from_start_time: typing.Optional[dt.datetime] = None, - to_start_time: typing.Optional[dt.datetime] = None, - version: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ObservationsViews: - """ - Get a list of observations - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - name : typing.Optional[str] - - user_id : typing.Optional[str] - - type : typing.Optional[str] - - trace_id : typing.Optional[str] - - parent_observation_id : typing.Optional[str] - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for observations where the environment is one of the provided values. - - from_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time on or after this datetime (ISO 8601). - - to_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time before this datetime (ISO 8601). - - version : typing.Optional[str] - Optional filter to only include observations with a certain version. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ObservationsViews - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.observations.get_many() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/observations", - method="GET", - params={ - "page": page, - "limit": limit, - "name": name, - "userId": user_id, - "type": type, - "traceId": trace_id, - "parentObservationId": parent_observation_id, - "environment": environment, - "fromStartTime": serialize_datetime(from_start_time) - if from_start_time is not None - else None, - "toStartTime": serialize_datetime(to_start_time) - if to_start_time is not None - else None, - "version": version, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ObservationsViews, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncObservationsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get( - self, - observation_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ObservationsView: - """ - Get a observation - - Parameters - ---------- - observation_id : str - The unique langfuse identifier of an observation, can be an event, span or generation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ObservationsView - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.observations.get( - observation_id="observationId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/observations/{jsonable_encoder(observation_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ObservationsView, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_many( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - name: typing.Optional[str] = None, - user_id: typing.Optional[str] = None, - type: typing.Optional[str] = None, - trace_id: typing.Optional[str] = None, - parent_observation_id: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - from_start_time: typing.Optional[dt.datetime] = None, - to_start_time: typing.Optional[dt.datetime] = None, - version: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ObservationsViews: - """ - Get a list of observations - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - name : typing.Optional[str] - - user_id : typing.Optional[str] - - type : typing.Optional[str] - - trace_id : typing.Optional[str] - - parent_observation_id : typing.Optional[str] - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for observations where the environment is one of the provided values. - - from_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time on or after this datetime (ISO 8601). - - to_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time before this datetime (ISO 8601). - - version : typing.Optional[str] - Optional filter to only include observations with a certain version. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ObservationsViews - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.observations.get_many() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/observations", - method="GET", - params={ - "page": page, - "limit": limit, - "name": name, - "userId": user_id, - "type": type, - "traceId": trace_id, - "parentObservationId": parent_observation_id, - "environment": environment, - "fromStartTime": serialize_datetime(from_start_time) - if from_start_time is not None - else None, - "toStartTime": serialize_datetime(to_start_time) - if to_start_time is not None - else None, - "version": version, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ObservationsViews, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/observations/types/__init__.py b/langfuse/api/resources/observations/types/__init__.py deleted file mode 100644 index 60f9d4e01..000000000 --- a/langfuse/api/resources/observations/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .observations import Observations -from .observations_views import ObservationsViews - -__all__ = ["Observations", "ObservationsViews"] diff --git a/langfuse/api/resources/observations/types/observations.py b/langfuse/api/resources/observations/types/observations.py deleted file mode 100644 index 1534dc87e..000000000 --- a/langfuse/api/resources/observations/types/observations.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.observation import Observation -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class Observations(pydantic_v1.BaseModel): - data: typing.List[Observation] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/observations/types/observations_views.py b/langfuse/api/resources/observations/types/observations_views.py deleted file mode 100644 index ed86b7d1e..000000000 --- a/langfuse/api/resources/observations/types/observations_views.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.observations_view import ObservationsView -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class ObservationsViews(pydantic_v1.BaseModel): - data: typing.List[ObservationsView] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/organizations/__init__.py b/langfuse/api/resources/organizations/__init__.py deleted file mode 100644 index 48edda3f4..000000000 --- a/langfuse/api/resources/organizations/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - MembershipRequest, - MembershipResponse, - MembershipRole, - MembershipsResponse, - OrganizationProject, - OrganizationProjectsResponse, -) - -__all__ = [ - "MembershipRequest", - "MembershipResponse", - "MembershipRole", - "MembershipsResponse", - "OrganizationProject", - "OrganizationProjectsResponse", -] diff --git a/langfuse/api/resources/organizations/client.py b/langfuse/api/resources/organizations/client.py deleted file mode 100644 index f7f2f5021..000000000 --- a/langfuse/api/resources/organizations/client.py +++ /dev/null @@ -1,750 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from .types.membership_request import MembershipRequest -from .types.membership_response import MembershipResponse -from .types.memberships_response import MembershipsResponse -from .types.organization_projects_response import OrganizationProjectsResponse - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class OrganizationsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get_organization_memberships( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> MembershipsResponse: - """ - Get all memberships for the organization associated with the API key (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembershipsResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.organizations.get_organization_memberships() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/organizations/memberships", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MembershipsResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_organization_membership( - self, - *, - request: MembershipRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> MembershipResponse: - """ - Create or update a membership for the organization associated with the API key (requires organization-scoped API key) - - Parameters - ---------- - request : MembershipRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembershipResponse - - Examples - -------- - from langfuse import MembershipRequest, MembershipRole - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.organizations.update_organization_membership( - request=MembershipRequest( - user_id="userId", - role=MembershipRole.OWNER, - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/organizations/memberships", - method="PUT", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MembershipResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_project_memberships( - self, - project_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> MembershipsResponse: - """ - Get all memberships for a specific project (requires organization-scoped API key) - - Parameters - ---------- - project_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembershipsResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.organizations.get_project_memberships( - project_id="projectId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/memberships", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MembershipsResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_project_membership( - self, - project_id: str, - *, - request: MembershipRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> MembershipResponse: - """ - Create or update a membership for a specific project (requires organization-scoped API key). The user must already be a member of the organization. - - Parameters - ---------- - project_id : str - - request : MembershipRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembershipResponse - - Examples - -------- - from langfuse import MembershipRequest, MembershipRole - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.organizations.update_project_membership( - project_id="projectId", - request=MembershipRequest( - user_id="userId", - role=MembershipRole.OWNER, - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/memberships", - method="PUT", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MembershipResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_organization_projects( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> OrganizationProjectsResponse: - """ - Get all projects for the organization associated with the API key (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - OrganizationProjectsResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.organizations.get_organization_projects() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/organizations/projects", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - OrganizationProjectsResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncOrganizationsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get_organization_memberships( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> MembershipsResponse: - """ - Get all memberships for the organization associated with the API key (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembershipsResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.organizations.get_organization_memberships() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/organizations/memberships", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MembershipsResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_organization_membership( - self, - *, - request: MembershipRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> MembershipResponse: - """ - Create or update a membership for the organization associated with the API key (requires organization-scoped API key) - - Parameters - ---------- - request : MembershipRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembershipResponse - - Examples - -------- - import asyncio - - from langfuse import MembershipRequest, MembershipRole - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.organizations.update_organization_membership( - request=MembershipRequest( - user_id="userId", - role=MembershipRole.OWNER, - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/organizations/memberships", - method="PUT", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MembershipResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_project_memberships( - self, - project_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> MembershipsResponse: - """ - Get all memberships for a specific project (requires organization-scoped API key) - - Parameters - ---------- - project_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembershipsResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.organizations.get_project_memberships( - project_id="projectId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/memberships", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MembershipsResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_project_membership( - self, - project_id: str, - *, - request: MembershipRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> MembershipResponse: - """ - Create or update a membership for a specific project (requires organization-scoped API key). The user must already be a member of the organization. - - Parameters - ---------- - project_id : str - - request : MembershipRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembershipResponse - - Examples - -------- - import asyncio - - from langfuse import MembershipRequest, MembershipRole - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.organizations.update_project_membership( - project_id="projectId", - request=MembershipRequest( - user_id="userId", - role=MembershipRole.OWNER, - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/memberships", - method="PUT", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(MembershipResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_organization_projects( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> OrganizationProjectsResponse: - """ - Get all projects for the organization associated with the API key (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - OrganizationProjectsResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.organizations.get_organization_projects() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/organizations/projects", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - OrganizationProjectsResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/organizations/types/__init__.py b/langfuse/api/resources/organizations/types/__init__.py deleted file mode 100644 index 4a401124d..000000000 --- a/langfuse/api/resources/organizations/types/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .membership_request import MembershipRequest -from .membership_response import MembershipResponse -from .membership_role import MembershipRole -from .memberships_response import MembershipsResponse -from .organization_project import OrganizationProject -from .organization_projects_response import OrganizationProjectsResponse - -__all__ = [ - "MembershipRequest", - "MembershipResponse", - "MembershipRole", - "MembershipsResponse", - "OrganizationProject", - "OrganizationProjectsResponse", -] diff --git a/langfuse/api/resources/organizations/types/membership_request.py b/langfuse/api/resources/organizations/types/membership_request.py deleted file mode 100644 index a7f046f51..000000000 --- a/langfuse/api/resources/organizations/types/membership_request.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .membership_role import MembershipRole - - -class MembershipRequest(pydantic_v1.BaseModel): - user_id: str = pydantic_v1.Field(alias="userId") - role: MembershipRole - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/organizations/types/membership_response.py b/langfuse/api/resources/organizations/types/membership_response.py deleted file mode 100644 index e9d82f3c7..000000000 --- a/langfuse/api/resources/organizations/types/membership_response.py +++ /dev/null @@ -1,48 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .membership_role import MembershipRole - - -class MembershipResponse(pydantic_v1.BaseModel): - user_id: str = pydantic_v1.Field(alias="userId") - role: MembershipRole - email: str - name: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/organizations/types/memberships_response.py b/langfuse/api/resources/organizations/types/memberships_response.py deleted file mode 100644 index 0a8091449..000000000 --- a/langfuse/api/resources/organizations/types/memberships_response.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .membership_response import MembershipResponse - - -class MembershipsResponse(pydantic_v1.BaseModel): - memberships: typing.List[MembershipResponse] - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/organizations/types/organization_project.py b/langfuse/api/resources/organizations/types/organization_project.py deleted file mode 100644 index 87f245b9a..000000000 --- a/langfuse/api/resources/organizations/types/organization_project.py +++ /dev/null @@ -1,48 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class OrganizationProject(pydantic_v1.BaseModel): - id: str - name: str - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/organizations/types/organization_projects_response.py b/langfuse/api/resources/organizations/types/organization_projects_response.py deleted file mode 100644 index 1c939a3e0..000000000 --- a/langfuse/api/resources/organizations/types/organization_projects_response.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .organization_project import OrganizationProject - - -class OrganizationProjectsResponse(pydantic_v1.BaseModel): - projects: typing.List[OrganizationProject] - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/projects/__init__.py b/langfuse/api/resources/projects/__init__.py deleted file mode 100644 index 26c74c1c7..000000000 --- a/langfuse/api/resources/projects/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - ApiKeyDeletionResponse, - ApiKeyList, - ApiKeyResponse, - ApiKeySummary, - Project, - ProjectDeletionResponse, - Projects, -) - -__all__ = [ - "ApiKeyDeletionResponse", - "ApiKeyList", - "ApiKeyResponse", - "ApiKeySummary", - "Project", - "ProjectDeletionResponse", - "Projects", -] diff --git a/langfuse/api/resources/projects/client.py b/langfuse/api/resources/projects/client.py deleted file mode 100644 index 2c63e3186..000000000 --- a/langfuse/api/resources/projects/client.py +++ /dev/null @@ -1,1090 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from .types.api_key_deletion_response import ApiKeyDeletionResponse -from .types.api_key_list import ApiKeyList -from .types.api_key_response import ApiKeyResponse -from .types.project import Project -from .types.project_deletion_response import ProjectDeletionResponse -from .types.projects import Projects - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class ProjectsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> Projects: - """ - Get Project associated with API key - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Projects - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.projects.get() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/projects", method="GET", request_options=request_options - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Projects, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, - *, - name: str, - retention: int, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Project: - """ - Create a new project (requires organization-scoped API key) - - Parameters - ---------- - name : str - - retention : int - Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. - - metadata : typing.Optional[typing.Dict[str, typing.Any]] - Optional metadata for the project - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Project - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.projects.create( - name="name", - retention=1, - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/projects", - method="POST", - json={"name": name, "metadata": metadata, "retention": retention}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Project, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update( - self, - project_id: str, - *, - name: str, - retention: int, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Project: - """ - Update a project by ID (requires organization-scoped API key). - - Parameters - ---------- - project_id : str - - name : str - - retention : int - Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. - - metadata : typing.Optional[typing.Dict[str, typing.Any]] - Optional metadata for the project - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Project - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.projects.update( - project_id="projectId", - name="name", - retention=1, - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}", - method="PUT", - json={"name": name, "metadata": metadata, "retention": retention}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Project, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete( - self, - project_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ProjectDeletionResponse: - """ - Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously. - - Parameters - ---------- - project_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ProjectDeletionResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.projects.delete( - project_id="projectId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - ProjectDeletionResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_api_keys( - self, - project_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiKeyList: - """ - Get all API keys for a project (requires organization-scoped API key) - - Parameters - ---------- - project_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiKeyList - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.projects.get_api_keys( - project_id="projectId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ApiKeyList, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create_api_key( - self, - project_id: str, - *, - note: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiKeyResponse: - """ - Create a new API key for a project (requires organization-scoped API key) - - Parameters - ---------- - project_id : str - - note : typing.Optional[str] - Optional note for the API key - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiKeyResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.projects.create_api_key( - project_id="projectId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", - method="POST", - json={"note": note}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ApiKeyResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_api_key( - self, - project_id: str, - api_key_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiKeyDeletionResponse: - """ - Delete an API key for a project (requires organization-scoped API key) - - Parameters - ---------- - project_id : str - - api_key_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiKeyDeletionResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.projects.delete_api_key( - project_id="projectId", - api_key_id="apiKeyId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys/{jsonable_encoder(api_key_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - ApiKeyDeletionResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncProjectsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> Projects: - """ - Get Project associated with API key - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Projects - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.projects.get() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/projects", method="GET", request_options=request_options - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Projects, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, - *, - name: str, - retention: int, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Project: - """ - Create a new project (requires organization-scoped API key) - - Parameters - ---------- - name : str - - retention : int - Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. - - metadata : typing.Optional[typing.Dict[str, typing.Any]] - Optional metadata for the project - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Project - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.projects.create( - name="name", - retention=1, - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/projects", - method="POST", - json={"name": name, "metadata": metadata, "retention": retention}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Project, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update( - self, - project_id: str, - *, - name: str, - retention: int, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Project: - """ - Update a project by ID (requires organization-scoped API key). - - Parameters - ---------- - project_id : str - - name : str - - retention : int - Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional. - - metadata : typing.Optional[typing.Dict[str, typing.Any]] - Optional metadata for the project - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Project - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.projects.update( - project_id="projectId", - name="name", - retention=1, - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}", - method="PUT", - json={"name": name, "metadata": metadata, "retention": retention}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Project, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete( - self, - project_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ProjectDeletionResponse: - """ - Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously. - - Parameters - ---------- - project_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ProjectDeletionResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.projects.delete( - project_id="projectId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - ProjectDeletionResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_api_keys( - self, - project_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiKeyList: - """ - Get all API keys for a project (requires organization-scoped API key) - - Parameters - ---------- - project_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiKeyList - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.projects.get_api_keys( - project_id="projectId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ApiKeyList, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create_api_key( - self, - project_id: str, - *, - note: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiKeyResponse: - """ - Create a new API key for a project (requires organization-scoped API key) - - Parameters - ---------- - project_id : str - - note : typing.Optional[str] - Optional note for the API key - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiKeyResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.projects.create_api_key( - project_id="projectId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", - method="POST", - json={"note": note}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ApiKeyResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_api_key( - self, - project_id: str, - api_key_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiKeyDeletionResponse: - """ - Delete an API key for a project (requires organization-scoped API key) - - Parameters - ---------- - project_id : str - - api_key_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiKeyDeletionResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.projects.delete_api_key( - project_id="projectId", - api_key_id="apiKeyId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys/{jsonable_encoder(api_key_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - ApiKeyDeletionResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/projects/types/__init__.py b/langfuse/api/resources/projects/types/__init__.py deleted file mode 100644 index c59b62a62..000000000 --- a/langfuse/api/resources/projects/types/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .api_key_deletion_response import ApiKeyDeletionResponse -from .api_key_list import ApiKeyList -from .api_key_response import ApiKeyResponse -from .api_key_summary import ApiKeySummary -from .project import Project -from .project_deletion_response import ProjectDeletionResponse -from .projects import Projects - -__all__ = [ - "ApiKeyDeletionResponse", - "ApiKeyList", - "ApiKeyResponse", - "ApiKeySummary", - "Project", - "ProjectDeletionResponse", - "Projects", -] diff --git a/langfuse/api/resources/projects/types/api_key_deletion_response.py b/langfuse/api/resources/projects/types/api_key_deletion_response.py deleted file mode 100644 index 6084400de..000000000 --- a/langfuse/api/resources/projects/types/api_key_deletion_response.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ApiKeyDeletionResponse(pydantic_v1.BaseModel): - """ - Response for API key deletion - """ - - success: bool - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/projects/types/api_key_list.py b/langfuse/api/resources/projects/types/api_key_list.py deleted file mode 100644 index 0a798ddbf..000000000 --- a/langfuse/api/resources/projects/types/api_key_list.py +++ /dev/null @@ -1,49 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .api_key_summary import ApiKeySummary - - -class ApiKeyList(pydantic_v1.BaseModel): - """ - List of API keys for a project - """ - - api_keys: typing.List[ApiKeySummary] = pydantic_v1.Field(alias="apiKeys") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/projects/types/api_key_response.py b/langfuse/api/resources/projects/types/api_key_response.py deleted file mode 100644 index fc9364faf..000000000 --- a/langfuse/api/resources/projects/types/api_key_response.py +++ /dev/null @@ -1,53 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ApiKeyResponse(pydantic_v1.BaseModel): - """ - Response for API key creation - """ - - id: str - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - public_key: str = pydantic_v1.Field(alias="publicKey") - secret_key: str = pydantic_v1.Field(alias="secretKey") - display_secret_key: str = pydantic_v1.Field(alias="displaySecretKey") - note: typing.Optional[str] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/projects/types/api_key_summary.py b/langfuse/api/resources/projects/types/api_key_summary.py deleted file mode 100644 index b95633731..000000000 --- a/langfuse/api/resources/projects/types/api_key_summary.py +++ /dev/null @@ -1,58 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ApiKeySummary(pydantic_v1.BaseModel): - """ - Summary of an API key - """ - - id: str - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - expires_at: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="expiresAt", default=None - ) - last_used_at: typing.Optional[dt.datetime] = pydantic_v1.Field( - alias="lastUsedAt", default=None - ) - note: typing.Optional[str] = None - public_key: str = pydantic_v1.Field(alias="publicKey") - display_secret_key: str = pydantic_v1.Field(alias="displaySecretKey") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/projects/types/project.py b/langfuse/api/resources/projects/types/project.py deleted file mode 100644 index cf257d406..000000000 --- a/langfuse/api/resources/projects/types/project.py +++ /dev/null @@ -1,56 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class Project(pydantic_v1.BaseModel): - id: str - name: str - metadata: typing.Dict[str, typing.Any] = pydantic_v1.Field() - """ - Metadata for the project - """ - - retention_days: typing.Optional[int] = pydantic_v1.Field( - alias="retentionDays", default=None - ) - """ - Number of days to retain data. Null or 0 means no retention. Omitted if no retention is configured. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/projects/types/project_deletion_response.py b/langfuse/api/resources/projects/types/project_deletion_response.py deleted file mode 100644 index 62c05d3d8..000000000 --- a/langfuse/api/resources/projects/types/project_deletion_response.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ProjectDeletionResponse(pydantic_v1.BaseModel): - success: bool - message: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/projects/types/projects.py b/langfuse/api/resources/projects/types/projects.py deleted file mode 100644 index c5eaabfbd..000000000 --- a/langfuse/api/resources/projects/types/projects.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .project import Project - - -class Projects(pydantic_v1.BaseModel): - data: typing.List[Project] - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompt_version/client.py b/langfuse/api/resources/prompt_version/client.py deleted file mode 100644 index 89140941d..000000000 --- a/langfuse/api/resources/prompt_version/client.py +++ /dev/null @@ -1,197 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..prompts.types.prompt import Prompt - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class PromptVersionClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def update( - self, - name: str, - version: int, - *, - new_labels: typing.Sequence[str], - request_options: typing.Optional[RequestOptions] = None, - ) -> Prompt: - """ - Update labels for a specific prompt version - - Parameters - ---------- - name : str - The name of the prompt - - version : int - Version of the prompt to update - - new_labels : typing.Sequence[str] - New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Prompt - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.prompt_version.update( - name="name", - version=1, - new_labels=["newLabels", "newLabels"], - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/v2/prompts/{jsonable_encoder(name)}/versions/{jsonable_encoder(version)}", - method="PATCH", - json={"newLabels": new_labels}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Prompt, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncPromptVersionClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def update( - self, - name: str, - version: int, - *, - new_labels: typing.Sequence[str], - request_options: typing.Optional[RequestOptions] = None, - ) -> Prompt: - """ - Update labels for a specific prompt version - - Parameters - ---------- - name : str - The name of the prompt - - version : int - Version of the prompt to update - - new_labels : typing.Sequence[str] - New labels for the prompt version. Labels are unique across versions. The "latest" label is reserved and managed by Langfuse. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Prompt - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.prompt_version.update( - name="name", - version=1, - new_labels=["newLabels", "newLabels"], - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/v2/prompts/{jsonable_encoder(name)}/versions/{jsonable_encoder(version)}", - method="PATCH", - json={"newLabels": new_labels}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Prompt, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/prompts/__init__.py b/langfuse/api/resources/prompts/__init__.py deleted file mode 100644 index 77c27486d..000000000 --- a/langfuse/api/resources/prompts/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - BasePrompt, - ChatMessage, - ChatMessageWithPlaceholders, - ChatMessageWithPlaceholders_Chatmessage, - ChatMessageWithPlaceholders_Placeholder, - ChatPrompt, - CreateChatPromptRequest, - CreatePromptRequest, - CreatePromptRequest_Chat, - CreatePromptRequest_Text, - CreateTextPromptRequest, - PlaceholderMessage, - Prompt, - PromptMeta, - PromptMetaListResponse, - Prompt_Chat, - Prompt_Text, - TextPrompt, -) - -__all__ = [ - "BasePrompt", - "ChatMessage", - "ChatMessageWithPlaceholders", - "ChatMessageWithPlaceholders_Chatmessage", - "ChatMessageWithPlaceholders_Placeholder", - "ChatPrompt", - "CreateChatPromptRequest", - "CreatePromptRequest", - "CreatePromptRequest_Chat", - "CreatePromptRequest_Text", - "CreateTextPromptRequest", - "PlaceholderMessage", - "Prompt", - "PromptMeta", - "PromptMetaListResponse", - "Prompt_Chat", - "Prompt_Text", - "TextPrompt", -] diff --git a/langfuse/api/resources/prompts/client.py b/langfuse/api/resources/prompts/client.py deleted file mode 100644 index c38c20156..000000000 --- a/langfuse/api/resources/prompts/client.py +++ /dev/null @@ -1,585 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.datetime_utils import serialize_datetime -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from .types.create_prompt_request import CreatePromptRequest -from .types.prompt import Prompt -from .types.prompt_meta_list_response import PromptMetaListResponse - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class PromptsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get( - self, - prompt_name: str, - *, - version: typing.Optional[int] = None, - label: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> Prompt: - """ - Get a prompt - - Parameters - ---------- - prompt_name : str - The name of the prompt - - version : typing.Optional[int] - Version of the prompt to be retrieved. - - label : typing.Optional[str] - Label of the prompt to be retrieved. Defaults to "production" if no label or version is set. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Prompt - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.prompts.get( - prompt_name="promptName", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}", - method="GET", - params={"version": version, "label": label}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Prompt, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list( - self, - *, - name: typing.Optional[str] = None, - label: typing.Optional[str] = None, - tag: typing.Optional[str] = None, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - from_updated_at: typing.Optional[dt.datetime] = None, - to_updated_at: typing.Optional[dt.datetime] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PromptMetaListResponse: - """ - Get a list of prompt names with versions and labels - - Parameters - ---------- - name : typing.Optional[str] - - label : typing.Optional[str] - - tag : typing.Optional[str] - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - from_updated_at : typing.Optional[dt.datetime] - Optional filter to only include prompt versions created/updated on or after a certain datetime (ISO 8601) - - to_updated_at : typing.Optional[dt.datetime] - Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PromptMetaListResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.prompts.list() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/v2/prompts", - method="GET", - params={ - "name": name, - "label": label, - "tag": tag, - "page": page, - "limit": limit, - "fromUpdatedAt": serialize_datetime(from_updated_at) - if from_updated_at is not None - else None, - "toUpdatedAt": serialize_datetime(to_updated_at) - if to_updated_at is not None - else None, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - PromptMetaListResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, - *, - request: CreatePromptRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> Prompt: - """ - Create a new version for the prompt with the given `name` - - Parameters - ---------- - request : CreatePromptRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Prompt - - Examples - -------- - from langfuse import ( - ChatMessageWithPlaceholders_Chatmessage, - CreatePromptRequest_Chat, - ) - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.prompts.create( - request=CreatePromptRequest_Chat( - name="name", - prompt=[ - ChatMessageWithPlaceholders_Chatmessage( - role="role", - content="content", - ), - ChatMessageWithPlaceholders_Chatmessage( - role="role", - content="content", - ), - ], - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/v2/prompts", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Prompt, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncPromptsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get( - self, - prompt_name: str, - *, - version: typing.Optional[int] = None, - label: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> Prompt: - """ - Get a prompt - - Parameters - ---------- - prompt_name : str - The name of the prompt - - version : typing.Optional[int] - Version of the prompt to be retrieved. - - label : typing.Optional[str] - Label of the prompt to be retrieved. Defaults to "production" if no label or version is set. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Prompt - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.prompts.get( - prompt_name="promptName", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/v2/prompts/{jsonable_encoder(prompt_name)}", - method="GET", - params={"version": version, "label": label}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Prompt, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list( - self, - *, - name: typing.Optional[str] = None, - label: typing.Optional[str] = None, - tag: typing.Optional[str] = None, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - from_updated_at: typing.Optional[dt.datetime] = None, - to_updated_at: typing.Optional[dt.datetime] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PromptMetaListResponse: - """ - Get a list of prompt names with versions and labels - - Parameters - ---------- - name : typing.Optional[str] - - label : typing.Optional[str] - - tag : typing.Optional[str] - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - from_updated_at : typing.Optional[dt.datetime] - Optional filter to only include prompt versions created/updated on or after a certain datetime (ISO 8601) - - to_updated_at : typing.Optional[dt.datetime] - Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PromptMetaListResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.prompts.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/v2/prompts", - method="GET", - params={ - "name": name, - "label": label, - "tag": tag, - "page": page, - "limit": limit, - "fromUpdatedAt": serialize_datetime(from_updated_at) - if from_updated_at is not None - else None, - "toUpdatedAt": serialize_datetime(to_updated_at) - if to_updated_at is not None - else None, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as( - PromptMetaListResponse, _response.json() - ) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, - *, - request: CreatePromptRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> Prompt: - """ - Create a new version for the prompt with the given `name` - - Parameters - ---------- - request : CreatePromptRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Prompt - - Examples - -------- - import asyncio - - from langfuse import ( - ChatMessageWithPlaceholders_Chatmessage, - CreatePromptRequest_Chat, - ) - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.prompts.create( - request=CreatePromptRequest_Chat( - name="name", - prompt=[ - ChatMessageWithPlaceholders_Chatmessage( - role="role", - content="content", - ), - ChatMessageWithPlaceholders_Chatmessage( - role="role", - content="content", - ), - ], - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/v2/prompts", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Prompt, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/prompts/types/__init__.py b/langfuse/api/resources/prompts/types/__init__.py deleted file mode 100644 index 3067f9f04..000000000 --- a/langfuse/api/resources/prompts/types/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .base_prompt import BasePrompt -from .chat_message import ChatMessage -from .chat_message_with_placeholders import ( - ChatMessageWithPlaceholders, - ChatMessageWithPlaceholders_Chatmessage, - ChatMessageWithPlaceholders_Placeholder, -) -from .chat_prompt import ChatPrompt -from .create_chat_prompt_request import CreateChatPromptRequest -from .create_prompt_request import ( - CreatePromptRequest, - CreatePromptRequest_Chat, - CreatePromptRequest_Text, -) -from .create_text_prompt_request import CreateTextPromptRequest -from .placeholder_message import PlaceholderMessage -from .prompt import Prompt, Prompt_Chat, Prompt_Text -from .prompt_meta import PromptMeta -from .prompt_meta_list_response import PromptMetaListResponse -from .text_prompt import TextPrompt - -__all__ = [ - "BasePrompt", - "ChatMessage", - "ChatMessageWithPlaceholders", - "ChatMessageWithPlaceholders_Chatmessage", - "ChatMessageWithPlaceholders_Placeholder", - "ChatPrompt", - "CreateChatPromptRequest", - "CreatePromptRequest", - "CreatePromptRequest_Chat", - "CreatePromptRequest_Text", - "CreateTextPromptRequest", - "PlaceholderMessage", - "Prompt", - "PromptMeta", - "PromptMetaListResponse", - "Prompt_Chat", - "Prompt_Text", - "TextPrompt", -] diff --git a/langfuse/api/resources/prompts/types/base_prompt.py b/langfuse/api/resources/prompts/types/base_prompt.py deleted file mode 100644 index eff295cc5..000000000 --- a/langfuse/api/resources/prompts/types/base_prompt.py +++ /dev/null @@ -1,69 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class BasePrompt(pydantic_v1.BaseModel): - name: str - version: int - config: typing.Any - labels: typing.List[str] = pydantic_v1.Field() - """ - List of deployment labels of this prompt version. - """ - - tags: typing.List[str] = pydantic_v1.Field() - """ - List of tags. Used to filter via UI and API. The same across versions of a prompt. - """ - - commit_message: typing.Optional[str] = pydantic_v1.Field( - alias="commitMessage", default=None - ) - """ - Commit message for this prompt version. - """ - - resolution_graph: typing.Optional[typing.Dict[str, typing.Any]] = pydantic_v1.Field( - alias="resolutionGraph", default=None - ) - """ - The dependency resolution graph for the current prompt. Null if prompt has no dependencies. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompts/types/chat_message.py b/langfuse/api/resources/prompts/types/chat_message.py deleted file mode 100644 index d009bc8cf..000000000 --- a/langfuse/api/resources/prompts/types/chat_message.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ChatMessage(pydantic_v1.BaseModel): - role: str - content: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompts/types/chat_message_with_placeholders.py b/langfuse/api/resources/prompts/types/chat_message_with_placeholders.py deleted file mode 100644 index dc12d5073..000000000 --- a/langfuse/api/resources/prompts/types/chat_message_with_placeholders.py +++ /dev/null @@ -1,87 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ChatMessageWithPlaceholders_Chatmessage(pydantic_v1.BaseModel): - role: str - content: str - type: typing.Literal["chatmessage"] = "chatmessage" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class ChatMessageWithPlaceholders_Placeholder(pydantic_v1.BaseModel): - name: str - type: typing.Literal["placeholder"] = "placeholder" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -ChatMessageWithPlaceholders = typing.Union[ - ChatMessageWithPlaceholders_Chatmessage, ChatMessageWithPlaceholders_Placeholder -] diff --git a/langfuse/api/resources/prompts/types/chat_prompt.py b/langfuse/api/resources/prompts/types/chat_prompt.py deleted file mode 100644 index 494449ea2..000000000 --- a/langfuse/api/resources/prompts/types/chat_prompt.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_prompt import BasePrompt -from .chat_message_with_placeholders import ChatMessageWithPlaceholders - - -class ChatPrompt(BasePrompt): - prompt: typing.List[ChatMessageWithPlaceholders] - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompts/types/create_chat_prompt_request.py b/langfuse/api/resources/prompts/types/create_chat_prompt_request.py deleted file mode 100644 index 1442164a6..000000000 --- a/langfuse/api/resources/prompts/types/create_chat_prompt_request.py +++ /dev/null @@ -1,63 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .chat_message_with_placeholders import ChatMessageWithPlaceholders - - -class CreateChatPromptRequest(pydantic_v1.BaseModel): - name: str - prompt: typing.List[ChatMessageWithPlaceholders] - config: typing.Optional[typing.Any] = None - labels: typing.Optional[typing.List[str]] = pydantic_v1.Field(default=None) - """ - List of deployment labels of this prompt version. - """ - - tags: typing.Optional[typing.List[str]] = pydantic_v1.Field(default=None) - """ - List of tags to apply to all versions of this prompt. - """ - - commit_message: typing.Optional[str] = pydantic_v1.Field( - alias="commitMessage", default=None - ) - """ - Commit message for this prompt version. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompts/types/create_prompt_request.py b/langfuse/api/resources/prompts/types/create_prompt_request.py deleted file mode 100644 index b9518a7c4..000000000 --- a/langfuse/api/resources/prompts/types/create_prompt_request.py +++ /dev/null @@ -1,103 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .chat_message_with_placeholders import ChatMessageWithPlaceholders - - -class CreatePromptRequest_Chat(pydantic_v1.BaseModel): - name: str - prompt: typing.List[ChatMessageWithPlaceholders] - config: typing.Optional[typing.Any] = None - labels: typing.Optional[typing.List[str]] = None - tags: typing.Optional[typing.List[str]] = None - commit_message: typing.Optional[str] = pydantic_v1.Field( - alias="commitMessage", default=None - ) - type: typing.Literal["chat"] = "chat" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class CreatePromptRequest_Text(pydantic_v1.BaseModel): - name: str - prompt: str - config: typing.Optional[typing.Any] = None - labels: typing.Optional[typing.List[str]] = None - tags: typing.Optional[typing.List[str]] = None - commit_message: typing.Optional[str] = pydantic_v1.Field( - alias="commitMessage", default=None - ) - type: typing.Literal["text"] = "text" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -CreatePromptRequest = typing.Union[CreatePromptRequest_Chat, CreatePromptRequest_Text] diff --git a/langfuse/api/resources/prompts/types/create_text_prompt_request.py b/langfuse/api/resources/prompts/types/create_text_prompt_request.py deleted file mode 100644 index d35fbb24d..000000000 --- a/langfuse/api/resources/prompts/types/create_text_prompt_request.py +++ /dev/null @@ -1,62 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class CreateTextPromptRequest(pydantic_v1.BaseModel): - name: str - prompt: str - config: typing.Optional[typing.Any] = None - labels: typing.Optional[typing.List[str]] = pydantic_v1.Field(default=None) - """ - List of deployment labels of this prompt version. - """ - - tags: typing.Optional[typing.List[str]] = pydantic_v1.Field(default=None) - """ - List of tags to apply to all versions of this prompt. - """ - - commit_message: typing.Optional[str] = pydantic_v1.Field( - alias="commitMessage", default=None - ) - """ - Commit message for this prompt version. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompts/types/placeholder_message.py b/langfuse/api/resources/prompts/types/placeholder_message.py deleted file mode 100644 index a3352b391..000000000 --- a/langfuse/api/resources/prompts/types/placeholder_message.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class PlaceholderMessage(pydantic_v1.BaseModel): - name: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompts/types/prompt.py b/langfuse/api/resources/prompts/types/prompt.py deleted file mode 100644 index 1ad894879..000000000 --- a/langfuse/api/resources/prompts/types/prompt.py +++ /dev/null @@ -1,111 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .chat_message_with_placeholders import ChatMessageWithPlaceholders - - -class Prompt_Chat(pydantic_v1.BaseModel): - prompt: typing.List[ChatMessageWithPlaceholders] - name: str - version: int - config: typing.Any - labels: typing.List[str] - tags: typing.List[str] - commit_message: typing.Optional[str] = pydantic_v1.Field( - alias="commitMessage", default=None - ) - resolution_graph: typing.Optional[typing.Dict[str, typing.Any]] = pydantic_v1.Field( - alias="resolutionGraph", default=None - ) - type: typing.Literal["chat"] = "chat" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class Prompt_Text(pydantic_v1.BaseModel): - prompt: str - name: str - version: int - config: typing.Any - labels: typing.List[str] - tags: typing.List[str] - commit_message: typing.Optional[str] = pydantic_v1.Field( - alias="commitMessage", default=None - ) - resolution_graph: typing.Optional[typing.Dict[str, typing.Any]] = pydantic_v1.Field( - alias="resolutionGraph", default=None - ) - type: typing.Literal["text"] = "text" - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -Prompt = typing.Union[Prompt_Chat, Prompt_Text] diff --git a/langfuse/api/resources/prompts/types/prompt_meta.py b/langfuse/api/resources/prompts/types/prompt_meta.py deleted file mode 100644 index bbb028fb2..000000000 --- a/langfuse/api/resources/prompts/types/prompt_meta.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class PromptMeta(pydantic_v1.BaseModel): - name: str - versions: typing.List[int] - labels: typing.List[str] - tags: typing.List[str] - last_updated_at: dt.datetime = pydantic_v1.Field(alias="lastUpdatedAt") - last_config: typing.Any = pydantic_v1.Field(alias="lastConfig") - """ - Config object of the most recent prompt version that matches the filters (if any are provided) - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompts/types/prompt_meta_list_response.py b/langfuse/api/resources/prompts/types/prompt_meta_list_response.py deleted file mode 100644 index d3dccf650..000000000 --- a/langfuse/api/resources/prompts/types/prompt_meta_list_response.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...utils.resources.pagination.types.meta_response import MetaResponse -from .prompt_meta import PromptMeta - - -class PromptMetaListResponse(pydantic_v1.BaseModel): - data: typing.List[PromptMeta] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/prompts/types/text_prompt.py b/langfuse/api/resources/prompts/types/text_prompt.py deleted file mode 100644 index e149ea322..000000000 --- a/langfuse/api/resources/prompts/types/text_prompt.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .base_prompt import BasePrompt - - -class TextPrompt(BasePrompt): - prompt: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/__init__.py b/langfuse/api/resources/scim/__init__.py deleted file mode 100644 index 29655a8da..000000000 --- a/langfuse/api/resources/scim/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - AuthenticationScheme, - BulkConfig, - EmptyResponse, - FilterConfig, - ResourceMeta, - ResourceType, - ResourceTypesResponse, - SchemaExtension, - SchemaResource, - SchemasResponse, - ScimEmail, - ScimFeatureSupport, - ScimName, - ScimUser, - ScimUsersListResponse, - ServiceProviderConfig, - UserMeta, -) - -__all__ = [ - "AuthenticationScheme", - "BulkConfig", - "EmptyResponse", - "FilterConfig", - "ResourceMeta", - "ResourceType", - "ResourceTypesResponse", - "SchemaExtension", - "SchemaResource", - "SchemasResponse", - "ScimEmail", - "ScimFeatureSupport", - "ScimName", - "ScimUser", - "ScimUsersListResponse", - "ServiceProviderConfig", - "UserMeta", -] diff --git a/langfuse/api/resources/scim/client.py b/langfuse/api/resources/scim/client.py deleted file mode 100644 index 38523a4f9..000000000 --- a/langfuse/api/resources/scim/client.py +++ /dev/null @@ -1,1042 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from .types.empty_response import EmptyResponse -from .types.resource_types_response import ResourceTypesResponse -from .types.schemas_response import SchemasResponse -from .types.scim_email import ScimEmail -from .types.scim_name import ScimName -from .types.scim_user import ScimUser -from .types.scim_users_list_response import ScimUsersListResponse -from .types.service_provider_config import ServiceProviderConfig - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class ScimClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get_service_provider_config( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ServiceProviderConfig: - """ - Get SCIM Service Provider Configuration (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ServiceProviderConfig - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scim.get_service_provider_config() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/scim/ServiceProviderConfig", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ServiceProviderConfig, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_resource_types( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ResourceTypesResponse: - """ - Get SCIM Resource Types (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ResourceTypesResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scim.get_resource_types() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/scim/ResourceTypes", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ResourceTypesResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_schemas( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> SchemasResponse: - """ - Get SCIM Schemas (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SchemasResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scim.get_schemas() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/scim/Schemas", method="GET", request_options=request_options - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(SchemasResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_users( - self, - *, - filter: typing.Optional[str] = None, - start_index: typing.Optional[int] = None, - count: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ScimUsersListResponse: - """ - List users in the organization (requires organization-scoped API key) - - Parameters - ---------- - filter : typing.Optional[str] - Filter expression (e.g. userName eq "value") - - start_index : typing.Optional[int] - 1-based index of the first result to return (default 1) - - count : typing.Optional[int] - Maximum number of results to return (default 100) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScimUsersListResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scim.list_users() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/scim/Users", - method="GET", - params={"filter": filter, "startIndex": start_index, "count": count}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScimUsersListResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create_user( - self, - *, - user_name: str, - name: ScimName, - emails: typing.Optional[typing.Sequence[ScimEmail]] = OMIT, - active: typing.Optional[bool] = OMIT, - password: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ScimUser: - """ - Create a new user in the organization (requires organization-scoped API key) - - Parameters - ---------- - user_name : str - User's email address (required) - - name : ScimName - User's name information - - emails : typing.Optional[typing.Sequence[ScimEmail]] - User's email addresses - - active : typing.Optional[bool] - Whether the user is active - - password : typing.Optional[str] - Initial password for the user - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScimUser - - Examples - -------- - from langfuse import ScimName - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scim.create_user( - user_name="userName", - name=ScimName(), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/scim/Users", - method="POST", - json={ - "userName": user_name, - "name": name, - "emails": emails, - "active": active, - "password": password, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScimUser, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_user( - self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ScimUser: - """ - Get a specific user by ID (requires organization-scoped API key) - - Parameters - ---------- - user_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScimUser - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scim.get_user( - user_id="userId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/scim/Users/{jsonable_encoder(user_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScimUser, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_user( - self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmptyResponse: - """ - Remove a user from the organization (requires organization-scoped API key). Note that this only removes the user from the organization but does not delete the user entity itself. - - Parameters - ---------- - user_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmptyResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scim.delete_user( - user_id="userId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/scim/Users/{jsonable_encoder(user_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(EmptyResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncScimClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get_service_provider_config( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ServiceProviderConfig: - """ - Get SCIM Service Provider Configuration (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ServiceProviderConfig - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scim.get_service_provider_config() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/scim/ServiceProviderConfig", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ServiceProviderConfig, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_resource_types( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ResourceTypesResponse: - """ - Get SCIM Resource Types (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ResourceTypesResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scim.get_resource_types() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/scim/ResourceTypes", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ResourceTypesResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_schemas( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> SchemasResponse: - """ - Get SCIM Schemas (requires organization-scoped API key) - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SchemasResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scim.get_schemas() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/scim/Schemas", method="GET", request_options=request_options - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(SchemasResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_users( - self, - *, - filter: typing.Optional[str] = None, - start_index: typing.Optional[int] = None, - count: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ScimUsersListResponse: - """ - List users in the organization (requires organization-scoped API key) - - Parameters - ---------- - filter : typing.Optional[str] - Filter expression (e.g. userName eq "value") - - start_index : typing.Optional[int] - 1-based index of the first result to return (default 1) - - count : typing.Optional[int] - Maximum number of results to return (default 100) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScimUsersListResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scim.list_users() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/scim/Users", - method="GET", - params={"filter": filter, "startIndex": start_index, "count": count}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScimUsersListResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create_user( - self, - *, - user_name: str, - name: ScimName, - emails: typing.Optional[typing.Sequence[ScimEmail]] = OMIT, - active: typing.Optional[bool] = OMIT, - password: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ScimUser: - """ - Create a new user in the organization (requires organization-scoped API key) - - Parameters - ---------- - user_name : str - User's email address (required) - - name : ScimName - User's name information - - emails : typing.Optional[typing.Sequence[ScimEmail]] - User's email addresses - - active : typing.Optional[bool] - Whether the user is active - - password : typing.Optional[str] - Initial password for the user - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScimUser - - Examples - -------- - import asyncio - - from langfuse import ScimName - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scim.create_user( - user_name="userName", - name=ScimName(), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/scim/Users", - method="POST", - json={ - "userName": user_name, - "name": name, - "emails": emails, - "active": active, - "password": password, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScimUser, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_user( - self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ScimUser: - """ - Get a specific user by ID (requires organization-scoped API key) - - Parameters - ---------- - user_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScimUser - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scim.get_user( - user_id="userId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/scim/Users/{jsonable_encoder(user_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScimUser, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_user( - self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmptyResponse: - """ - Remove a user from the organization (requires organization-scoped API key). Note that this only removes the user from the organization but does not delete the user entity itself. - - Parameters - ---------- - user_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmptyResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scim.delete_user( - user_id="userId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/scim/Users/{jsonable_encoder(user_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(EmptyResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/scim/types/__init__.py b/langfuse/api/resources/scim/types/__init__.py deleted file mode 100644 index c0b60e8c2..000000000 --- a/langfuse/api/resources/scim/types/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .authentication_scheme import AuthenticationScheme -from .bulk_config import BulkConfig -from .empty_response import EmptyResponse -from .filter_config import FilterConfig -from .resource_meta import ResourceMeta -from .resource_type import ResourceType -from .resource_types_response import ResourceTypesResponse -from .schema_extension import SchemaExtension -from .schema_resource import SchemaResource -from .schemas_response import SchemasResponse -from .scim_email import ScimEmail -from .scim_feature_support import ScimFeatureSupport -from .scim_name import ScimName -from .scim_user import ScimUser -from .scim_users_list_response import ScimUsersListResponse -from .service_provider_config import ServiceProviderConfig -from .user_meta import UserMeta - -__all__ = [ - "AuthenticationScheme", - "BulkConfig", - "EmptyResponse", - "FilterConfig", - "ResourceMeta", - "ResourceType", - "ResourceTypesResponse", - "SchemaExtension", - "SchemaResource", - "SchemasResponse", - "ScimEmail", - "ScimFeatureSupport", - "ScimName", - "ScimUser", - "ScimUsersListResponse", - "ServiceProviderConfig", - "UserMeta", -] diff --git a/langfuse/api/resources/scim/types/authentication_scheme.py b/langfuse/api/resources/scim/types/authentication_scheme.py deleted file mode 100644 index 6d6526901..000000000 --- a/langfuse/api/resources/scim/types/authentication_scheme.py +++ /dev/null @@ -1,48 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class AuthenticationScheme(pydantic_v1.BaseModel): - name: str - description: str - spec_uri: str = pydantic_v1.Field(alias="specUri") - type: str - primary: bool - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/bulk_config.py b/langfuse/api/resources/scim/types/bulk_config.py deleted file mode 100644 index 0b41af5cf..000000000 --- a/langfuse/api/resources/scim/types/bulk_config.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class BulkConfig(pydantic_v1.BaseModel): - supported: bool - max_operations: int = pydantic_v1.Field(alias="maxOperations") - max_payload_size: int = pydantic_v1.Field(alias="maxPayloadSize") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/empty_response.py b/langfuse/api/resources/scim/types/empty_response.py deleted file mode 100644 index 82105e8a3..000000000 --- a/langfuse/api/resources/scim/types/empty_response.py +++ /dev/null @@ -1,44 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class EmptyResponse(pydantic_v1.BaseModel): - """ - Empty response for 204 No Content responses - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/filter_config.py b/langfuse/api/resources/scim/types/filter_config.py deleted file mode 100644 index 2bd035867..000000000 --- a/langfuse/api/resources/scim/types/filter_config.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class FilterConfig(pydantic_v1.BaseModel): - supported: bool - max_results: int = pydantic_v1.Field(alias="maxResults") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/resource_meta.py b/langfuse/api/resources/scim/types/resource_meta.py deleted file mode 100644 index a61d14442..000000000 --- a/langfuse/api/resources/scim/types/resource_meta.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ResourceMeta(pydantic_v1.BaseModel): - resource_type: str = pydantic_v1.Field(alias="resourceType") - location: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/resource_type.py b/langfuse/api/resources/scim/types/resource_type.py deleted file mode 100644 index 264dc87cf..000000000 --- a/langfuse/api/resources/scim/types/resource_type.py +++ /dev/null @@ -1,55 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .resource_meta import ResourceMeta -from .schema_extension import SchemaExtension - - -class ResourceType(pydantic_v1.BaseModel): - schemas: typing.Optional[typing.List[str]] = None - id: str - name: str - endpoint: str - description: str - schema_: str = pydantic_v1.Field(alias="schema") - schema_extensions: typing.List[SchemaExtension] = pydantic_v1.Field( - alias="schemaExtensions" - ) - meta: ResourceMeta - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/resource_types_response.py b/langfuse/api/resources/scim/types/resource_types_response.py deleted file mode 100644 index cce65b8d1..000000000 --- a/langfuse/api/resources/scim/types/resource_types_response.py +++ /dev/null @@ -1,47 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .resource_type import ResourceType - - -class ResourceTypesResponse(pydantic_v1.BaseModel): - schemas: typing.List[str] - total_results: int = pydantic_v1.Field(alias="totalResults") - resources: typing.List[ResourceType] = pydantic_v1.Field(alias="Resources") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/schema_extension.py b/langfuse/api/resources/scim/types/schema_extension.py deleted file mode 100644 index c5ede44b9..000000000 --- a/langfuse/api/resources/scim/types/schema_extension.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class SchemaExtension(pydantic_v1.BaseModel): - schema_: str = pydantic_v1.Field(alias="schema") - required: bool - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/schema_resource.py b/langfuse/api/resources/scim/types/schema_resource.py deleted file mode 100644 index e85cda9a0..000000000 --- a/langfuse/api/resources/scim/types/schema_resource.py +++ /dev/null @@ -1,47 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .resource_meta import ResourceMeta - - -class SchemaResource(pydantic_v1.BaseModel): - id: str - name: str - description: str - attributes: typing.List[typing.Any] - meta: ResourceMeta - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/schemas_response.py b/langfuse/api/resources/scim/types/schemas_response.py deleted file mode 100644 index 4c7b8199a..000000000 --- a/langfuse/api/resources/scim/types/schemas_response.py +++ /dev/null @@ -1,47 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .schema_resource import SchemaResource - - -class SchemasResponse(pydantic_v1.BaseModel): - schemas: typing.List[str] - total_results: int = pydantic_v1.Field(alias="totalResults") - resources: typing.List[SchemaResource] = pydantic_v1.Field(alias="Resources") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/scim_email.py b/langfuse/api/resources/scim/types/scim_email.py deleted file mode 100644 index 71b817809..000000000 --- a/langfuse/api/resources/scim/types/scim_email.py +++ /dev/null @@ -1,44 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ScimEmail(pydantic_v1.BaseModel): - primary: bool - value: str - type: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/scim_feature_support.py b/langfuse/api/resources/scim/types/scim_feature_support.py deleted file mode 100644 index 2aedc07b5..000000000 --- a/langfuse/api/resources/scim/types/scim_feature_support.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ScimFeatureSupport(pydantic_v1.BaseModel): - supported: bool - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/scim_name.py b/langfuse/api/resources/scim/types/scim_name.py deleted file mode 100644 index c2812a25a..000000000 --- a/langfuse/api/resources/scim/types/scim_name.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class ScimName(pydantic_v1.BaseModel): - formatted: typing.Optional[str] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/scim_user.py b/langfuse/api/resources/scim/types/scim_user.py deleted file mode 100644 index 581bab8c1..000000000 --- a/langfuse/api/resources/scim/types/scim_user.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .scim_email import ScimEmail -from .scim_name import ScimName -from .user_meta import UserMeta - - -class ScimUser(pydantic_v1.BaseModel): - schemas: typing.List[str] - id: str - user_name: str = pydantic_v1.Field(alias="userName") - name: ScimName - emails: typing.List[ScimEmail] - meta: UserMeta - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/scim_users_list_response.py b/langfuse/api/resources/scim/types/scim_users_list_response.py deleted file mode 100644 index 3c41a4d16..000000000 --- a/langfuse/api/resources/scim/types/scim_users_list_response.py +++ /dev/null @@ -1,49 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .scim_user import ScimUser - - -class ScimUsersListResponse(pydantic_v1.BaseModel): - schemas: typing.List[str] - total_results: int = pydantic_v1.Field(alias="totalResults") - start_index: int = pydantic_v1.Field(alias="startIndex") - items_per_page: int = pydantic_v1.Field(alias="itemsPerPage") - resources: typing.List[ScimUser] = pydantic_v1.Field(alias="Resources") - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/service_provider_config.py b/langfuse/api/resources/scim/types/service_provider_config.py deleted file mode 100644 index 9bf611ae6..000000000 --- a/langfuse/api/resources/scim/types/service_provider_config.py +++ /dev/null @@ -1,60 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from .authentication_scheme import AuthenticationScheme -from .bulk_config import BulkConfig -from .filter_config import FilterConfig -from .resource_meta import ResourceMeta -from .scim_feature_support import ScimFeatureSupport - - -class ServiceProviderConfig(pydantic_v1.BaseModel): - schemas: typing.List[str] - documentation_uri: str = pydantic_v1.Field(alias="documentationUri") - patch: ScimFeatureSupport - bulk: BulkConfig - filter: FilterConfig - change_password: ScimFeatureSupport = pydantic_v1.Field(alias="changePassword") - sort: ScimFeatureSupport - etag: ScimFeatureSupport - authentication_schemes: typing.List[AuthenticationScheme] = pydantic_v1.Field( - alias="authenticationSchemes" - ) - meta: ResourceMeta - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/scim/types/user_meta.py b/langfuse/api/resources/scim/types/user_meta.py deleted file mode 100644 index 09cb7e6a0..000000000 --- a/langfuse/api/resources/scim/types/user_meta.py +++ /dev/null @@ -1,48 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class UserMeta(pydantic_v1.BaseModel): - resource_type: str = pydantic_v1.Field(alias="resourceType") - created: typing.Optional[str] = None - last_modified: typing.Optional[str] = pydantic_v1.Field( - alias="lastModified", default=None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score/__init__.py b/langfuse/api/resources/score/__init__.py deleted file mode 100644 index 566310af3..000000000 --- a/langfuse/api/resources/score/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateScoreRequest, CreateScoreResponse - -__all__ = ["CreateScoreRequest", "CreateScoreResponse"] diff --git a/langfuse/api/resources/score/client.py b/langfuse/api/resources/score/client.py deleted file mode 100644 index 0c259929f..000000000 --- a/langfuse/api/resources/score/client.py +++ /dev/null @@ -1,322 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from .types.create_score_request import CreateScoreRequest -from .types.create_score_response import CreateScoreResponse - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class ScoreClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def create( - self, - *, - request: CreateScoreRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateScoreResponse: - """ - Create a score (supports both trace and session scores) - - Parameters - ---------- - request : CreateScoreRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreateScoreResponse - - Examples - -------- - from langfuse import CreateScoreRequest - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.score.create( - request=CreateScoreRequest( - name="name", - value=1.1, - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/scores", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(CreateScoreResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Delete a score (supports both trace and session scores) - - Parameters - ---------- - score_id : str - The unique langfuse identifier of a score - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.score.delete( - score_id="scoreId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/scores/{jsonable_encoder(score_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncScoreClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def create( - self, - *, - request: CreateScoreRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateScoreResponse: - """ - Create a score (supports both trace and session scores) - - Parameters - ---------- - request : CreateScoreRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreateScoreResponse - - Examples - -------- - import asyncio - - from langfuse import CreateScoreRequest - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.score.create( - request=CreateScoreRequest( - name="name", - value=1.1, - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/scores", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(CreateScoreResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Delete a score (supports both trace and session scores) - - Parameters - ---------- - score_id : str - The unique langfuse identifier of a score - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.score.delete( - score_id="scoreId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/scores/{jsonable_encoder(score_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/score/types/__init__.py b/langfuse/api/resources/score/types/__init__.py deleted file mode 100644 index 72d61f6f3..000000000 --- a/langfuse/api/resources/score/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_score_request import CreateScoreRequest -from .create_score_response import CreateScoreResponse - -__all__ = ["CreateScoreRequest", "CreateScoreResponse"] diff --git a/langfuse/api/resources/score/types/create_score_request.py b/langfuse/api/resources/score/types/create_score_request.py deleted file mode 100644 index d6ad037a4..000000000 --- a/langfuse/api/resources/score/types/create_score_request.py +++ /dev/null @@ -1,92 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.create_score_value import CreateScoreValue -from ...commons.types.score_data_type import ScoreDataType - - -class CreateScoreRequest(pydantic_v1.BaseModel): - """ - Examples - -------- - from langfuse import CreateScoreRequest - - CreateScoreRequest( - name="novelty", - value=0.9, - trace_id="cdef-1234-5678-90ab", - ) - """ - - id: typing.Optional[str] = None - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - value: CreateScoreValue = pydantic_v1.Field() - """ - The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false) - """ - - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - environment: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - """ - - data_type: typing.Optional[ScoreDataType] = pydantic_v1.Field( - alias="dataType", default=None - ) - """ - The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. - """ - - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - """ - Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score/types/create_score_response.py b/langfuse/api/resources/score/types/create_score_response.py deleted file mode 100644 index a8c90fce2..000000000 --- a/langfuse/api/resources/score/types/create_score_response.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class CreateScoreResponse(pydantic_v1.BaseModel): - id: str = pydantic_v1.Field() - """ - The id of the created object in Langfuse - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score_configs/__init__.py b/langfuse/api/resources/score_configs/__init__.py deleted file mode 100644 index e46e2a578..000000000 --- a/langfuse/api/resources/score_configs/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateScoreConfigRequest, ScoreConfigs - -__all__ = ["CreateScoreConfigRequest", "ScoreConfigs"] diff --git a/langfuse/api/resources/score_configs/client.py b/langfuse/api/resources/score_configs/client.py deleted file mode 100644 index 7bd2a72df..000000000 --- a/langfuse/api/resources/score_configs/client.py +++ /dev/null @@ -1,473 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.score_config import ScoreConfig -from .types.create_score_config_request import CreateScoreConfigRequest -from .types.score_configs import ScoreConfigs - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class ScoreConfigsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def create( - self, - *, - request: CreateScoreConfigRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> ScoreConfig: - """ - Create a score configuration (config). Score configs are used to define the structure of scores - - Parameters - ---------- - request : CreateScoreConfigRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScoreConfig - - Examples - -------- - from langfuse import CreateScoreConfigRequest, ScoreDataType - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.score_configs.create( - request=CreateScoreConfigRequest( - name="name", - data_type=ScoreDataType.NUMERIC, - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/score-configs", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScoreConfig, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ScoreConfigs: - """ - Get all score configs - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScoreConfigs - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.score_configs.get() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/score-configs", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScoreConfigs, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_by_id( - self, config_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ScoreConfig: - """ - Get a score config - - Parameters - ---------- - config_id : str - The unique langfuse identifier of a score config - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScoreConfig - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.score_configs.get_by_id( - config_id="configId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/score-configs/{jsonable_encoder(config_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScoreConfig, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncScoreConfigsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def create( - self, - *, - request: CreateScoreConfigRequest, - request_options: typing.Optional[RequestOptions] = None, - ) -> ScoreConfig: - """ - Create a score configuration (config). Score configs are used to define the structure of scores - - Parameters - ---------- - request : CreateScoreConfigRequest - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScoreConfig - - Examples - -------- - import asyncio - - from langfuse import CreateScoreConfigRequest, ScoreDataType - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.score_configs.create( - request=CreateScoreConfigRequest( - name="name", - data_type=ScoreDataType.NUMERIC, - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/score-configs", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScoreConfig, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ScoreConfigs: - """ - Get all score configs - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScoreConfigs - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.score_configs.get() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/score-configs", - method="GET", - params={"page": page, "limit": limit}, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScoreConfigs, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_by_id( - self, config_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ScoreConfig: - """ - Get a score config - - Parameters - ---------- - config_id : str - The unique langfuse identifier of a score config - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ScoreConfig - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.score_configs.get_by_id( - config_id="configId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/score-configs/{jsonable_encoder(config_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(ScoreConfig, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/score_configs/types/__init__.py b/langfuse/api/resources/score_configs/types/__init__.py deleted file mode 100644 index 401650f2b..000000000 --- a/langfuse/api/resources/score_configs/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_score_config_request import CreateScoreConfigRequest -from .score_configs import ScoreConfigs - -__all__ = ["CreateScoreConfigRequest", "ScoreConfigs"] diff --git a/langfuse/api/resources/score_configs/types/create_score_config_request.py b/langfuse/api/resources/score_configs/types/create_score_config_request.py deleted file mode 100644 index e136af157..000000000 --- a/langfuse/api/resources/score_configs/types/create_score_config_request.py +++ /dev/null @@ -1,72 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.config_category import ConfigCategory -from ...commons.types.score_data_type import ScoreDataType - - -class CreateScoreConfigRequest(pydantic_v1.BaseModel): - name: str - data_type: ScoreDataType = pydantic_v1.Field(alias="dataType") - categories: typing.Optional[typing.List[ConfigCategory]] = pydantic_v1.Field( - default=None - ) - """ - Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed - """ - - min_value: typing.Optional[float] = pydantic_v1.Field( - alias="minValue", default=None - ) - """ - Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ - """ - - max_value: typing.Optional[float] = pydantic_v1.Field( - alias="maxValue", default=None - ) - """ - Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ - """ - - description: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score_configs/types/score_configs.py b/langfuse/api/resources/score_configs/types/score_configs.py deleted file mode 100644 index fc84e28a3..000000000 --- a/langfuse/api/resources/score_configs/types/score_configs.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.score_config import ScoreConfig -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class ScoreConfigs(pydantic_v1.BaseModel): - data: typing.List[ScoreConfig] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score_v_2/__init__.py b/langfuse/api/resources/score_v_2/__init__.py deleted file mode 100644 index 40599eec1..000000000 --- a/langfuse/api/resources/score_v_2/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - GetScoresResponse, - GetScoresResponseData, - GetScoresResponseDataBoolean, - GetScoresResponseDataCategorical, - GetScoresResponseDataNumeric, - GetScoresResponseData_Boolean, - GetScoresResponseData_Categorical, - GetScoresResponseData_Numeric, - GetScoresResponseTraceData, -) - -__all__ = [ - "GetScoresResponse", - "GetScoresResponseData", - "GetScoresResponseDataBoolean", - "GetScoresResponseDataCategorical", - "GetScoresResponseDataNumeric", - "GetScoresResponseData_Boolean", - "GetScoresResponseData_Categorical", - "GetScoresResponseData_Numeric", - "GetScoresResponseTraceData", -] diff --git a/langfuse/api/resources/score_v_2/types/__init__.py b/langfuse/api/resources/score_v_2/types/__init__.py deleted file mode 100644 index 480ed3406..000000000 --- a/langfuse/api/resources/score_v_2/types/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .get_scores_response import GetScoresResponse -from .get_scores_response_data import ( - GetScoresResponseData, - GetScoresResponseData_Boolean, - GetScoresResponseData_Categorical, - GetScoresResponseData_Numeric, -) -from .get_scores_response_data_boolean import GetScoresResponseDataBoolean -from .get_scores_response_data_categorical import GetScoresResponseDataCategorical -from .get_scores_response_data_numeric import GetScoresResponseDataNumeric -from .get_scores_response_trace_data import GetScoresResponseTraceData - -__all__ = [ - "GetScoresResponse", - "GetScoresResponseData", - "GetScoresResponseDataBoolean", - "GetScoresResponseDataCategorical", - "GetScoresResponseDataNumeric", - "GetScoresResponseData_Boolean", - "GetScoresResponseData_Categorical", - "GetScoresResponseData_Numeric", - "GetScoresResponseTraceData", -] diff --git a/langfuse/api/resources/score_v_2/types/get_scores_response.py b/langfuse/api/resources/score_v_2/types/get_scores_response.py deleted file mode 100644 index 777bb799b..000000000 --- a/langfuse/api/resources/score_v_2/types/get_scores_response.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...utils.resources.pagination.types.meta_response import MetaResponse -from .get_scores_response_data import GetScoresResponseData - - -class GetScoresResponse(pydantic_v1.BaseModel): - data: typing.List[GetScoresResponseData] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score_v_2/types/get_scores_response_data.py b/langfuse/api/resources/score_v_2/types/get_scores_response_data.py deleted file mode 100644 index 63f41f4c4..000000000 --- a/langfuse/api/resources/score_v_2/types/get_scores_response_data.py +++ /dev/null @@ -1,215 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.score_source import ScoreSource -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseData_Numeric(pydantic_v1.BaseModel): - trace: typing.Optional[GetScoresResponseTraceData] = None - value: float - id: str - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["NUMERIC"] = pydantic_v1.Field( - alias="dataType", default="NUMERIC" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class GetScoresResponseData_Categorical(pydantic_v1.BaseModel): - trace: typing.Optional[GetScoresResponseTraceData] = None - value: typing.Optional[float] = None - string_value: str = pydantic_v1.Field(alias="stringValue") - id: str - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["CATEGORICAL"] = pydantic_v1.Field( - alias="dataType", default="CATEGORICAL" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -class GetScoresResponseData_Boolean(pydantic_v1.BaseModel): - trace: typing.Optional[GetScoresResponseTraceData] = None - value: float - string_value: str = pydantic_v1.Field(alias="stringValue") - id: str - trace_id: typing.Optional[str] = pydantic_v1.Field(alias="traceId", default=None) - session_id: typing.Optional[str] = pydantic_v1.Field( - alias="sessionId", default=None - ) - observation_id: typing.Optional[str] = pydantic_v1.Field( - alias="observationId", default=None - ) - dataset_run_id: typing.Optional[str] = pydantic_v1.Field( - alias="datasetRunId", default=None - ) - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: dt.datetime = pydantic_v1.Field(alias="createdAt") - updated_at: dt.datetime = pydantic_v1.Field(alias="updatedAt") - author_user_id: typing.Optional[str] = pydantic_v1.Field( - alias="authorUserId", default=None - ) - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - config_id: typing.Optional[str] = pydantic_v1.Field(alias="configId", default=None) - queue_id: typing.Optional[str] = pydantic_v1.Field(alias="queueId", default=None) - environment: typing.Optional[str] = None - data_type: typing.Literal["BOOLEAN"] = pydantic_v1.Field( - alias="dataType", default="BOOLEAN" - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} - - -GetScoresResponseData = typing.Union[ - GetScoresResponseData_Numeric, - GetScoresResponseData_Categorical, - GetScoresResponseData_Boolean, -] diff --git a/langfuse/api/resources/score_v_2/types/get_scores_response_data_boolean.py b/langfuse/api/resources/score_v_2/types/get_scores_response_data_boolean.py deleted file mode 100644 index 48012990c..000000000 --- a/langfuse/api/resources/score_v_2/types/get_scores_response_data_boolean.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.boolean_score import BooleanScore -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseDataBoolean(BooleanScore): - trace: typing.Optional[GetScoresResponseTraceData] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score_v_2/types/get_scores_response_data_categorical.py b/langfuse/api/resources/score_v_2/types/get_scores_response_data_categorical.py deleted file mode 100644 index 6e27f6d64..000000000 --- a/langfuse/api/resources/score_v_2/types/get_scores_response_data_categorical.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.categorical_score import CategoricalScore -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseDataCategorical(CategoricalScore): - trace: typing.Optional[GetScoresResponseTraceData] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score_v_2/types/get_scores_response_data_numeric.py b/langfuse/api/resources/score_v_2/types/get_scores_response_data_numeric.py deleted file mode 100644 index f7342833f..000000000 --- a/langfuse/api/resources/score_v_2/types/get_scores_response_data_numeric.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.numeric_score import NumericScore -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseDataNumeric(NumericScore): - trace: typing.Optional[GetScoresResponseTraceData] = None - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/score_v_2/types/get_scores_response_trace_data.py b/langfuse/api/resources/score_v_2/types/get_scores_response_trace_data.py deleted file mode 100644 index 6e5539e35..000000000 --- a/langfuse/api/resources/score_v_2/types/get_scores_response_trace_data.py +++ /dev/null @@ -1,57 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class GetScoresResponseTraceData(pydantic_v1.BaseModel): - user_id: typing.Optional[str] = pydantic_v1.Field(alias="userId", default=None) - """ - The user ID associated with the trace referenced by score - """ - - tags: typing.Optional[typing.List[str]] = pydantic_v1.Field(default=None) - """ - A list of tags associated with the trace referenced by score - """ - - environment: typing.Optional[str] = pydantic_v1.Field(default=None) - """ - The environment of the trace referenced by score - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/sessions/__init__.py b/langfuse/api/resources/sessions/__init__.py deleted file mode 100644 index 048704297..000000000 --- a/langfuse/api/resources/sessions/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import PaginatedSessions - -__all__ = ["PaginatedSessions"] diff --git a/langfuse/api/resources/sessions/client.py b/langfuse/api/resources/sessions/client.py deleted file mode 100644 index d5ae779c3..000000000 --- a/langfuse/api/resources/sessions/client.py +++ /dev/null @@ -1,367 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.datetime_utils import serialize_datetime -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.session_with_traces import SessionWithTraces -from .types.paginated_sessions import PaginatedSessions - - -class SessionsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedSessions: - """ - Get sessions - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created before a certain datetime (ISO 8601) - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for sessions where the environment is one of the provided values. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedSessions - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.sessions.list() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/sessions", - method="GET", - params={ - "page": page, - "limit": limit, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "environment": environment, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedSessions, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get( - self, - session_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> SessionWithTraces: - """ - Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=` - - Parameters - ---------- - session_id : str - The unique id of a session - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SessionWithTraces - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.sessions.get( - session_id="sessionId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/sessions/{jsonable_encoder(session_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(SessionWithTraces, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncSessionsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedSessions: - """ - Get sessions - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created before a certain datetime (ISO 8601) - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for sessions where the environment is one of the provided values. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedSessions - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.sessions.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/sessions", - method="GET", - params={ - "page": page, - "limit": limit, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "environment": environment, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(PaginatedSessions, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get( - self, - session_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> SessionWithTraces: - """ - Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=` - - Parameters - ---------- - session_id : str - The unique id of a session - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SessionWithTraces - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.sessions.get( - session_id="sessionId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/sessions/{jsonable_encoder(session_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(SessionWithTraces, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/sessions/types/__init__.py b/langfuse/api/resources/sessions/types/__init__.py deleted file mode 100644 index 42d63b428..000000000 --- a/langfuse/api/resources/sessions/types/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .paginated_sessions import PaginatedSessions - -__all__ = ["PaginatedSessions"] diff --git a/langfuse/api/resources/sessions/types/paginated_sessions.py b/langfuse/api/resources/sessions/types/paginated_sessions.py deleted file mode 100644 index 5dd9fb497..000000000 --- a/langfuse/api/resources/sessions/types/paginated_sessions.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.session import Session -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class PaginatedSessions(pydantic_v1.BaseModel): - data: typing.List[Session] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/trace/__init__.py b/langfuse/api/resources/trace/__init__.py deleted file mode 100644 index 17855e971..000000000 --- a/langfuse/api/resources/trace/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import DeleteTraceResponse, Sort, Traces - -__all__ = ["DeleteTraceResponse", "Sort", "Traces"] diff --git a/langfuse/api/resources/trace/client.py b/langfuse/api/resources/trace/client.py deleted file mode 100644 index c73901123..000000000 --- a/langfuse/api/resources/trace/client.py +++ /dev/null @@ -1,725 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.datetime_utils import serialize_datetime -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.trace_with_full_details import TraceWithFullDetails -from .types.delete_trace_response import DeleteTraceResponse -from .types.traces import Traces - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class TraceClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get( - self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> TraceWithFullDetails: - """ - Get a specific trace - - Parameters - ---------- - trace_id : str - The unique langfuse identifier of a trace - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - TraceWithFullDetails - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.trace.get( - trace_id="traceId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/traces/{jsonable_encoder(trace_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(TraceWithFullDetails, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete( - self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> DeleteTraceResponse: - """ - Delete a specific trace - - Parameters - ---------- - trace_id : str - The unique langfuse identifier of the trace to delete - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteTraceResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.trace.delete( - trace_id="traceId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/traces/{jsonable_encoder(trace_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DeleteTraceResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - order_by: typing.Optional[str] = None, - tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - version: typing.Optional[str] = None, - release: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> Traces: - """ - Get list of traces - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - user_id : typing.Optional[str] - - name : typing.Optional[str] - - session_id : typing.Optional[str] - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) - - order_by : typing.Optional[str] - Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc - - tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only traces that include all of these tags will be returned. - - version : typing.Optional[str] - Optional filter to only include traces with a certain version. - - release : typing.Optional[str] - Optional filter to only include traces with a certain release. - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for traces where the environment is one of the provided values. - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups are 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not provided, all fields are included. Example: 'core,scores,metrics' - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Traces - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.trace.list() - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/traces", - method="GET", - params={ - "page": page, - "limit": limit, - "userId": user_id, - "name": name, - "sessionId": session_id, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "orderBy": order_by, - "tags": tags, - "version": version, - "release": release, - "environment": environment, - "fields": fields, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Traces, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_multiple( - self, - *, - trace_ids: typing.Sequence[str], - request_options: typing.Optional[RequestOptions] = None, - ) -> DeleteTraceResponse: - """ - Delete multiple traces - - Parameters - ---------- - trace_ids : typing.Sequence[str] - List of trace IDs to delete - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteTraceResponse - - Examples - -------- - from langfuse.client import FernLangfuse - - client = FernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.trace.delete_multiple( - trace_ids=["traceIds", "traceIds"], - ) - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/traces", - method="DELETE", - json={"traceIds": trace_ids}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DeleteTraceResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncTraceClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get( - self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> TraceWithFullDetails: - """ - Get a specific trace - - Parameters - ---------- - trace_id : str - The unique langfuse identifier of a trace - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - TraceWithFullDetails - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.trace.get( - trace_id="traceId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/traces/{jsonable_encoder(trace_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(TraceWithFullDetails, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete( - self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> DeleteTraceResponse: - """ - Delete a specific trace - - Parameters - ---------- - trace_id : str - The unique langfuse identifier of the trace to delete - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteTraceResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.trace.delete( - trace_id="traceId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/traces/{jsonable_encoder(trace_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DeleteTraceResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - order_by: typing.Optional[str] = None, - tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - version: typing.Optional[str] = None, - release: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> Traces: - """ - Get list of traces - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - user_id : typing.Optional[str] - - name : typing.Optional[str] - - session_id : typing.Optional[str] - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) - - order_by : typing.Optional[str] - Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc - - tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only traces that include all of these tags will be returned. - - version : typing.Optional[str] - Optional filter to only include traces with a certain version. - - release : typing.Optional[str] - Optional filter to only include traces with a certain release. - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for traces where the environment is one of the provided values. - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups are 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not provided, all fields are included. Example: 'core,scores,metrics' - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Traces - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.trace.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/traces", - method="GET", - params={ - "page": page, - "limit": limit, - "userId": user_id, - "name": name, - "sessionId": session_id, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "orderBy": order_by, - "tags": tags, - "version": version, - "release": release, - "environment": environment, - "fields": fields, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Traces, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_multiple( - self, - *, - trace_ids: typing.Sequence[str], - request_options: typing.Optional[RequestOptions] = None, - ) -> DeleteTraceResponse: - """ - Delete multiple traces - - Parameters - ---------- - trace_ids : typing.Sequence[str] - List of trace IDs to delete - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteTraceResponse - - Examples - -------- - import asyncio - - from langfuse.client import AsyncFernLangfuse - - client = AsyncFernLangfuse( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.trace.delete_multiple( - trace_ids=["traceIds", "traceIds"], - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/traces", - method="DELETE", - json={"traceIds": trace_ids}, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(DeleteTraceResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/langfuse/api/resources/trace/types/__init__.py b/langfuse/api/resources/trace/types/__init__.py deleted file mode 100644 index 929a1e047..000000000 --- a/langfuse/api/resources/trace/types/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .delete_trace_response import DeleteTraceResponse -from .sort import Sort -from .traces import Traces - -__all__ = ["DeleteTraceResponse", "Sort", "Traces"] diff --git a/langfuse/api/resources/trace/types/delete_trace_response.py b/langfuse/api/resources/trace/types/delete_trace_response.py deleted file mode 100644 index 450c894e2..000000000 --- a/langfuse/api/resources/trace/types/delete_trace_response.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class DeleteTraceResponse(pydantic_v1.BaseModel): - message: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/trace/types/sort.py b/langfuse/api/resources/trace/types/sort.py deleted file mode 100644 index 76a5045b6..000000000 --- a/langfuse/api/resources/trace/types/sort.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class Sort(pydantic_v1.BaseModel): - id: str - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/trace/types/traces.py b/langfuse/api/resources/trace/types/traces.py deleted file mode 100644 index 09f58978f..000000000 --- a/langfuse/api/resources/trace/types/traces.py +++ /dev/null @@ -1,45 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ....core.datetime_utils import serialize_datetime -from ....core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 -from ...commons.types.trace_with_details import TraceWithDetails -from ...utils.resources.pagination.types.meta_response import MetaResponse - - -class Traces(pydantic_v1.BaseModel): - data: typing.List[TraceWithDetails] - meta: MetaResponse - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/resources/utils/__init__.py b/langfuse/api/resources/utils/__init__.py deleted file mode 100644 index b4ac87b8a..000000000 --- a/langfuse/api/resources/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .resources import MetaResponse, pagination - -__all__ = ["MetaResponse", "pagination"] diff --git a/langfuse/api/resources/utils/resources/__init__.py b/langfuse/api/resources/utils/resources/__init__.py deleted file mode 100644 index 7e65ff270..000000000 --- a/langfuse/api/resources/utils/resources/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from . import pagination -from .pagination import MetaResponse - -__all__ = ["MetaResponse", "pagination"] diff --git a/langfuse/api/resources/utils/resources/pagination/types/__init__.py b/langfuse/api/resources/utils/resources/pagination/types/__init__.py deleted file mode 100644 index 79bb6018e..000000000 --- a/langfuse/api/resources/utils/resources/pagination/types/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .meta_response import MetaResponse - -__all__ = ["MetaResponse"] diff --git a/langfuse/api/resources/utils/resources/pagination/types/meta_response.py b/langfuse/api/resources/utils/resources/pagination/types/meta_response.py deleted file mode 100644 index 2d082c68f..000000000 --- a/langfuse/api/resources/utils/resources/pagination/types/meta_response.py +++ /dev/null @@ -1,62 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ......core.datetime_utils import serialize_datetime -from ......core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 - - -class MetaResponse(pydantic_v1.BaseModel): - page: int = pydantic_v1.Field() - """ - current page number - """ - - limit: int = pydantic_v1.Field() - """ - number of items per page - """ - - total_items: int = pydantic_v1.Field(alias="totalItems") - """ - number of total items given the current filters/selection (if any) - """ - - total_pages: int = pydantic_v1.Field(alias="totalPages") - """ - number of total pages given the current limit - """ - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults_exclude_unset: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - kwargs_with_defaults_exclude_none: typing.Any = { - "by_alias": True, - "exclude_none": True, - **kwargs, - } - - return deep_union_pydantic_dicts( - super().dict(**kwargs_with_defaults_exclude_unset), - super().dict(**kwargs_with_defaults_exclude_none), - ) - - class Config: - frozen = True - smart_union = True - allow_population_by_field_name = True - populate_by_name = True - extra = pydantic_v1.Extra.allow - json_encoders = {dt.datetime: serialize_datetime} diff --git a/langfuse/api/scim/__init__.py b/langfuse/api/scim/__init__.py new file mode 100644 index 000000000..6c4126f3c --- /dev/null +++ b/langfuse/api/scim/__init__.py @@ -0,0 +1,94 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + AuthenticationScheme, + BulkConfig, + EmptyResponse, + FilterConfig, + ResourceMeta, + ResourceType, + ResourceTypesResponse, + SchemaExtension, + SchemaResource, + SchemasResponse, + ScimEmail, + ScimFeatureSupport, + ScimName, + ScimUser, + ScimUsersListResponse, + ServiceProviderConfig, + UserMeta, + ) +_dynamic_imports: typing.Dict[str, str] = { + "AuthenticationScheme": ".types", + "BulkConfig": ".types", + "EmptyResponse": ".types", + "FilterConfig": ".types", + "ResourceMeta": ".types", + "ResourceType": ".types", + "ResourceTypesResponse": ".types", + "SchemaExtension": ".types", + "SchemaResource": ".types", + "SchemasResponse": ".types", + "ScimEmail": ".types", + "ScimFeatureSupport": ".types", + "ScimName": ".types", + "ScimUser": ".types", + "ScimUsersListResponse": ".types", + "ServiceProviderConfig": ".types", + "UserMeta": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AuthenticationScheme", + "BulkConfig", + "EmptyResponse", + "FilterConfig", + "ResourceMeta", + "ResourceType", + "ResourceTypesResponse", + "SchemaExtension", + "SchemaResource", + "SchemasResponse", + "ScimEmail", + "ScimFeatureSupport", + "ScimName", + "ScimUser", + "ScimUsersListResponse", + "ServiceProviderConfig", + "UserMeta", +] diff --git a/langfuse/api/scim/client.py b/langfuse/api/scim/client.py new file mode 100644 index 000000000..30d0815bf --- /dev/null +++ b/langfuse/api/scim/client.py @@ -0,0 +1,686 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawScimClient, RawScimClient +from .types.empty_response import EmptyResponse +from .types.resource_types_response import ResourceTypesResponse +from .types.schemas_response import SchemasResponse +from .types.scim_email import ScimEmail +from .types.scim_name import ScimName +from .types.scim_user import ScimUser +from .types.scim_users_list_response import ScimUsersListResponse +from .types.service_provider_config import ServiceProviderConfig + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ScimClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawScimClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawScimClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawScimClient + """ + return self._raw_client + + def get_service_provider_config( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> ServiceProviderConfig: + """ + Get SCIM Service Provider Configuration (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ServiceProviderConfig + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scim.get_service_provider_config() + """ + _response = self._raw_client.get_service_provider_config( + request_options=request_options + ) + return _response.data + + def get_resource_types( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> ResourceTypesResponse: + """ + Get SCIM Resource Types (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ResourceTypesResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scim.get_resource_types() + """ + _response = self._raw_client.get_resource_types(request_options=request_options) + return _response.data + + def get_schemas( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> SchemasResponse: + """ + Get SCIM Schemas (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SchemasResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scim.get_schemas() + """ + _response = self._raw_client.get_schemas(request_options=request_options) + return _response.data + + def list_users( + self, + *, + filter: typing.Optional[str] = None, + start_index: typing.Optional[int] = None, + count: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScimUsersListResponse: + """ + List users in the organization (requires organization-scoped API key) + + Parameters + ---------- + filter : typing.Optional[str] + Filter expression (e.g. userName eq "value") + + start_index : typing.Optional[int] + 1-based index of the first result to return (default 1) + + count : typing.Optional[int] + Maximum number of results to return (default 100) + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScimUsersListResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scim.list_users() + """ + _response = self._raw_client.list_users( + filter=filter, + start_index=start_index, + count=count, + request_options=request_options, + ) + return _response.data + + def create_user( + self, + *, + user_name: str, + name: ScimName, + emails: typing.Optional[typing.Sequence[ScimEmail]] = OMIT, + active: typing.Optional[bool] = OMIT, + password: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScimUser: + """ + Create a new user in the organization (requires organization-scoped API key) + + Parameters + ---------- + user_name : str + User's email address (required) + + name : ScimName + User's name information + + emails : typing.Optional[typing.Sequence[ScimEmail]] + User's email addresses + + active : typing.Optional[bool] + Whether the user is active + + password : typing.Optional[str] + Initial password for the user + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScimUser + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.scim import ScimName + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scim.create_user( + user_name="userName", + name=ScimName(), + ) + """ + _response = self._raw_client.create_user( + user_name=user_name, + name=name, + emails=emails, + active=active, + password=password, + request_options=request_options, + ) + return _response.data + + def get_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ScimUser: + """ + Get a specific user by ID (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScimUser + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scim.get_user( + user_id="userId", + ) + """ + _response = self._raw_client.get_user(user_id, request_options=request_options) + return _response.data + + def delete_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> EmptyResponse: + """ + Remove a user from the organization (requires organization-scoped API key). Note that this only removes the user from the organization but does not delete the user entity itself. + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EmptyResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scim.delete_user( + user_id="userId", + ) + """ + _response = self._raw_client.delete_user( + user_id, request_options=request_options + ) + return _response.data + + +class AsyncScimClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawScimClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawScimClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawScimClient + """ + return self._raw_client + + async def get_service_provider_config( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> ServiceProviderConfig: + """ + Get SCIM Service Provider Configuration (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ServiceProviderConfig + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.scim.get_service_provider_config() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_service_provider_config( + request_options=request_options + ) + return _response.data + + async def get_resource_types( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> ResourceTypesResponse: + """ + Get SCIM Resource Types (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ResourceTypesResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.scim.get_resource_types() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_resource_types( + request_options=request_options + ) + return _response.data + + async def get_schemas( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> SchemasResponse: + """ + Get SCIM Schemas (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SchemasResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.scim.get_schemas() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_schemas(request_options=request_options) + return _response.data + + async def list_users( + self, + *, + filter: typing.Optional[str] = None, + start_index: typing.Optional[int] = None, + count: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScimUsersListResponse: + """ + List users in the organization (requires organization-scoped API key) + + Parameters + ---------- + filter : typing.Optional[str] + Filter expression (e.g. userName eq "value") + + start_index : typing.Optional[int] + 1-based index of the first result to return (default 1) + + count : typing.Optional[int] + Maximum number of results to return (default 100) + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScimUsersListResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.scim.list_users() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_users( + filter=filter, + start_index=start_index, + count=count, + request_options=request_options, + ) + return _response.data + + async def create_user( + self, + *, + user_name: str, + name: ScimName, + emails: typing.Optional[typing.Sequence[ScimEmail]] = OMIT, + active: typing.Optional[bool] = OMIT, + password: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScimUser: + """ + Create a new user in the organization (requires organization-scoped API key) + + Parameters + ---------- + user_name : str + User's email address (required) + + name : ScimName + User's name information + + emails : typing.Optional[typing.Sequence[ScimEmail]] + User's email addresses + + active : typing.Optional[bool] + Whether the user is active + + password : typing.Optional[str] + Initial password for the user + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScimUser + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.scim import ScimName + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.scim.create_user( + user_name="userName", + name=ScimName(), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_user( + user_name=user_name, + name=name, + emails=emails, + active=active, + password=password, + request_options=request_options, + ) + return _response.data + + async def get_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ScimUser: + """ + Get a specific user by ID (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScimUser + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.scim.get_user( + user_id="userId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_user( + user_id, request_options=request_options + ) + return _response.data + + async def delete_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> EmptyResponse: + """ + Remove a user from the organization (requires organization-scoped API key). Note that this only removes the user from the organization but does not delete the user entity itself. + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EmptyResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.scim.delete_user( + user_id="userId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_user( + user_id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/scim/raw_client.py b/langfuse/api/scim/raw_client.py new file mode 100644 index 000000000..e65f46592 --- /dev/null +++ b/langfuse/api/scim/raw_client.py @@ -0,0 +1,1528 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from .types.empty_response import EmptyResponse +from .types.resource_types_response import ResourceTypesResponse +from .types.schemas_response import SchemasResponse +from .types.scim_email import ScimEmail +from .types.scim_name import ScimName +from .types.scim_user import ScimUser +from .types.scim_users_list_response import ScimUsersListResponse +from .types.service_provider_config import ServiceProviderConfig + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawScimClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_service_provider_config( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ServiceProviderConfig]: + """ + Get SCIM Service Provider Configuration (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ServiceProviderConfig] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/scim/ServiceProviderConfig", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ServiceProviderConfig, + parse_obj_as( + type_=ServiceProviderConfig, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_resource_types( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ResourceTypesResponse]: + """ + Get SCIM Resource Types (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ResourceTypesResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/scim/ResourceTypes", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ResourceTypesResponse, + parse_obj_as( + type_=ResourceTypesResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_schemas( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[SchemasResponse]: + """ + Get SCIM Schemas (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SchemasResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/scim/Schemas", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SchemasResponse, + parse_obj_as( + type_=SchemasResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def list_users( + self, + *, + filter: typing.Optional[str] = None, + start_index: typing.Optional[int] = None, + count: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ScimUsersListResponse]: + """ + List users in the organization (requires organization-scoped API key) + + Parameters + ---------- + filter : typing.Optional[str] + Filter expression (e.g. userName eq "value") + + start_index : typing.Optional[int] + 1-based index of the first result to return (default 1) + + count : typing.Optional[int] + Maximum number of results to return (default 100) + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ScimUsersListResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/scim/Users", + method="GET", + params={ + "filter": filter, + "startIndex": start_index, + "count": count, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScimUsersListResponse, + parse_obj_as( + type_=ScimUsersListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create_user( + self, + *, + user_name: str, + name: ScimName, + emails: typing.Optional[typing.Sequence[ScimEmail]] = OMIT, + active: typing.Optional[bool] = OMIT, + password: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ScimUser]: + """ + Create a new user in the organization (requires organization-scoped API key) + + Parameters + ---------- + user_name : str + User's email address (required) + + name : ScimName + User's name information + + emails : typing.Optional[typing.Sequence[ScimEmail]] + User's email addresses + + active : typing.Optional[bool] + Whether the user is active + + password : typing.Optional[str] + Initial password for the user + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ScimUser] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/scim/Users", + method="POST", + json={ + "userName": user_name, + "name": convert_and_respect_annotation_metadata( + object_=name, annotation=ScimName, direction="write" + ), + "emails": convert_and_respect_annotation_metadata( + object_=emails, + annotation=typing.Sequence[ScimEmail], + direction="write", + ), + "active": active, + "password": password, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScimUser, + parse_obj_as( + type_=ScimUser, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ScimUser]: + """ + Get a specific user by ID (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ScimUser] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/scim/Users/{jsonable_encoder(user_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScimUser, + parse_obj_as( + type_=ScimUser, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[EmptyResponse]: + """ + Remove a user from the organization (requires organization-scoped API key). Note that this only removes the user from the organization but does not delete the user entity itself. + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[EmptyResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/scim/Users/{jsonable_encoder(user_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EmptyResponse, + parse_obj_as( + type_=EmptyResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawScimClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_service_provider_config( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ServiceProviderConfig]: + """ + Get SCIM Service Provider Configuration (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ServiceProviderConfig] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/scim/ServiceProviderConfig", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ServiceProviderConfig, + parse_obj_as( + type_=ServiceProviderConfig, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_resource_types( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ResourceTypesResponse]: + """ + Get SCIM Resource Types (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ResourceTypesResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/scim/ResourceTypes", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ResourceTypesResponse, + parse_obj_as( + type_=ResourceTypesResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_schemas( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[SchemasResponse]: + """ + Get SCIM Schemas (requires organization-scoped API key) + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SchemasResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/scim/Schemas", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SchemasResponse, + parse_obj_as( + type_=SchemasResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def list_users( + self, + *, + filter: typing.Optional[str] = None, + start_index: typing.Optional[int] = None, + count: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ScimUsersListResponse]: + """ + List users in the organization (requires organization-scoped API key) + + Parameters + ---------- + filter : typing.Optional[str] + Filter expression (e.g. userName eq "value") + + start_index : typing.Optional[int] + 1-based index of the first result to return (default 1) + + count : typing.Optional[int] + Maximum number of results to return (default 100) + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ScimUsersListResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/scim/Users", + method="GET", + params={ + "filter": filter, + "startIndex": start_index, + "count": count, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScimUsersListResponse, + parse_obj_as( + type_=ScimUsersListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create_user( + self, + *, + user_name: str, + name: ScimName, + emails: typing.Optional[typing.Sequence[ScimEmail]] = OMIT, + active: typing.Optional[bool] = OMIT, + password: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ScimUser]: + """ + Create a new user in the organization (requires organization-scoped API key) + + Parameters + ---------- + user_name : str + User's email address (required) + + name : ScimName + User's name information + + emails : typing.Optional[typing.Sequence[ScimEmail]] + User's email addresses + + active : typing.Optional[bool] + Whether the user is active + + password : typing.Optional[str] + Initial password for the user + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ScimUser] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/scim/Users", + method="POST", + json={ + "userName": user_name, + "name": convert_and_respect_annotation_metadata( + object_=name, annotation=ScimName, direction="write" + ), + "emails": convert_and_respect_annotation_metadata( + object_=emails, + annotation=typing.Sequence[ScimEmail], + direction="write", + ), + "active": active, + "password": password, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScimUser, + parse_obj_as( + type_=ScimUser, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ScimUser]: + """ + Get a specific user by ID (requires organization-scoped API key) + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ScimUser] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/scim/Users/{jsonable_encoder(user_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScimUser, + parse_obj_as( + type_=ScimUser, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[EmptyResponse]: + """ + Remove a user from the organization (requires organization-scoped API key). Note that this only removes the user from the organization but does not delete the user entity itself. + + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[EmptyResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/scim/Users/{jsonable_encoder(user_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EmptyResponse, + parse_obj_as( + type_=EmptyResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/scim/types/__init__.py b/langfuse/api/scim/types/__init__.py new file mode 100644 index 000000000..9d6483e3d --- /dev/null +++ b/langfuse/api/scim/types/__init__.py @@ -0,0 +1,92 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .authentication_scheme import AuthenticationScheme + from .bulk_config import BulkConfig + from .empty_response import EmptyResponse + from .filter_config import FilterConfig + from .resource_meta import ResourceMeta + from .resource_type import ResourceType + from .resource_types_response import ResourceTypesResponse + from .schema_extension import SchemaExtension + from .schema_resource import SchemaResource + from .schemas_response import SchemasResponse + from .scim_email import ScimEmail + from .scim_feature_support import ScimFeatureSupport + from .scim_name import ScimName + from .scim_user import ScimUser + from .scim_users_list_response import ScimUsersListResponse + from .service_provider_config import ServiceProviderConfig + from .user_meta import UserMeta +_dynamic_imports: typing.Dict[str, str] = { + "AuthenticationScheme": ".authentication_scheme", + "BulkConfig": ".bulk_config", + "EmptyResponse": ".empty_response", + "FilterConfig": ".filter_config", + "ResourceMeta": ".resource_meta", + "ResourceType": ".resource_type", + "ResourceTypesResponse": ".resource_types_response", + "SchemaExtension": ".schema_extension", + "SchemaResource": ".schema_resource", + "SchemasResponse": ".schemas_response", + "ScimEmail": ".scim_email", + "ScimFeatureSupport": ".scim_feature_support", + "ScimName": ".scim_name", + "ScimUser": ".scim_user", + "ScimUsersListResponse": ".scim_users_list_response", + "ServiceProviderConfig": ".service_provider_config", + "UserMeta": ".user_meta", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AuthenticationScheme", + "BulkConfig", + "EmptyResponse", + "FilterConfig", + "ResourceMeta", + "ResourceType", + "ResourceTypesResponse", + "SchemaExtension", + "SchemaResource", + "SchemasResponse", + "ScimEmail", + "ScimFeatureSupport", + "ScimName", + "ScimUser", + "ScimUsersListResponse", + "ServiceProviderConfig", + "UserMeta", +] diff --git a/langfuse/api/scim/types/authentication_scheme.py b/langfuse/api/scim/types/authentication_scheme.py new file mode 100644 index 000000000..fc1fceb14 --- /dev/null +++ b/langfuse/api/scim/types/authentication_scheme.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class AuthenticationScheme(UniversalBaseModel): + name: str + description: str + spec_uri: typing_extensions.Annotated[str, FieldMetadata(alias="specUri")] + type: str + primary: bool + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/bulk_config.py b/langfuse/api/scim/types/bulk_config.py new file mode 100644 index 000000000..4a3ae719f --- /dev/null +++ b/langfuse/api/scim/types/bulk_config.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class BulkConfig(UniversalBaseModel): + supported: bool + max_operations: typing_extensions.Annotated[ + int, FieldMetadata(alias="maxOperations") + ] + max_payload_size: typing_extensions.Annotated[ + int, FieldMetadata(alias="maxPayloadSize") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/empty_response.py b/langfuse/api/scim/types/empty_response.py new file mode 100644 index 000000000..1371104f8 --- /dev/null +++ b/langfuse/api/scim/types/empty_response.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class EmptyResponse(UniversalBaseModel): + """ + Empty response for 204 No Content responses + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/filter_config.py b/langfuse/api/scim/types/filter_config.py new file mode 100644 index 000000000..ba9986e56 --- /dev/null +++ b/langfuse/api/scim/types/filter_config.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class FilterConfig(UniversalBaseModel): + supported: bool + max_results: typing_extensions.Annotated[int, FieldMetadata(alias="maxResults")] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/resource_meta.py b/langfuse/api/scim/types/resource_meta.py new file mode 100644 index 000000000..99be2a96e --- /dev/null +++ b/langfuse/api/scim/types/resource_meta.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class ResourceMeta(UniversalBaseModel): + resource_type: typing_extensions.Annotated[str, FieldMetadata(alias="resourceType")] + location: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/resource_type.py b/langfuse/api/scim/types/resource_type.py new file mode 100644 index 000000000..9913c465d --- /dev/null +++ b/langfuse/api/scim/types/resource_type.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .resource_meta import ResourceMeta +from .schema_extension import SchemaExtension + + +class ResourceType(UniversalBaseModel): + schemas: typing.Optional[typing.List[str]] = None + id: str + name: str + endpoint: str + description: str + schema_: typing_extensions.Annotated[str, FieldMetadata(alias="schema")] + schema_extensions: typing_extensions.Annotated[ + typing.List[SchemaExtension], FieldMetadata(alias="schemaExtensions") + ] + meta: ResourceMeta + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/resource_types_response.py b/langfuse/api/scim/types/resource_types_response.py new file mode 100644 index 000000000..8ff1c47d9 --- /dev/null +++ b/langfuse/api/scim/types/resource_types_response.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .resource_type import ResourceType + + +class ResourceTypesResponse(UniversalBaseModel): + schemas: typing.List[str] + total_results: typing_extensions.Annotated[int, FieldMetadata(alias="totalResults")] + resources: typing_extensions.Annotated[ + typing.List[ResourceType], FieldMetadata(alias="Resources") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/schema_extension.py b/langfuse/api/scim/types/schema_extension.py new file mode 100644 index 000000000..4a09d6192 --- /dev/null +++ b/langfuse/api/scim/types/schema_extension.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class SchemaExtension(UniversalBaseModel): + schema_: typing_extensions.Annotated[str, FieldMetadata(alias="schema")] + required: bool + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/schema_resource.py b/langfuse/api/scim/types/schema_resource.py new file mode 100644 index 000000000..cafc07dcb --- /dev/null +++ b/langfuse/api/scim/types/schema_resource.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .resource_meta import ResourceMeta + + +class SchemaResource(UniversalBaseModel): + id: str + name: str + description: str + attributes: typing.List[typing.Any] + meta: ResourceMeta + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/schemas_response.py b/langfuse/api/scim/types/schemas_response.py new file mode 100644 index 000000000..3162f9431 --- /dev/null +++ b/langfuse/api/scim/types/schemas_response.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .schema_resource import SchemaResource + + +class SchemasResponse(UniversalBaseModel): + schemas: typing.List[str] + total_results: typing_extensions.Annotated[int, FieldMetadata(alias="totalResults")] + resources: typing_extensions.Annotated[ + typing.List[SchemaResource], FieldMetadata(alias="Resources") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/scim_email.py b/langfuse/api/scim/types/scim_email.py new file mode 100644 index 000000000..7d589f0cf --- /dev/null +++ b/langfuse/api/scim/types/scim_email.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ScimEmail(UniversalBaseModel): + primary: bool + value: str + type: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/scim_feature_support.py b/langfuse/api/scim/types/scim_feature_support.py new file mode 100644 index 000000000..4c24a694d --- /dev/null +++ b/langfuse/api/scim/types/scim_feature_support.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ScimFeatureSupport(UniversalBaseModel): + supported: bool + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/scim_name.py b/langfuse/api/scim/types/scim_name.py new file mode 100644 index 000000000..53d2d79e3 --- /dev/null +++ b/langfuse/api/scim/types/scim_name.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ScimName(UniversalBaseModel): + formatted: typing.Optional[str] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/scim_user.py b/langfuse/api/scim/types/scim_user.py new file mode 100644 index 000000000..22d6d50fe --- /dev/null +++ b/langfuse/api/scim/types/scim_user.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .scim_email import ScimEmail +from .scim_name import ScimName +from .user_meta import UserMeta + + +class ScimUser(UniversalBaseModel): + schemas: typing.List[str] + id: str + user_name: typing_extensions.Annotated[str, FieldMetadata(alias="userName")] + name: ScimName + emails: typing.List[ScimEmail] + meta: UserMeta + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/scim_users_list_response.py b/langfuse/api/scim/types/scim_users_list_response.py new file mode 100644 index 000000000..bcfba30bb --- /dev/null +++ b/langfuse/api/scim/types/scim_users_list_response.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .scim_user import ScimUser + + +class ScimUsersListResponse(UniversalBaseModel): + schemas: typing.List[str] + total_results: typing_extensions.Annotated[int, FieldMetadata(alias="totalResults")] + start_index: typing_extensions.Annotated[int, FieldMetadata(alias="startIndex")] + items_per_page: typing_extensions.Annotated[ + int, FieldMetadata(alias="itemsPerPage") + ] + resources: typing_extensions.Annotated[ + typing.List[ScimUser], FieldMetadata(alias="Resources") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/service_provider_config.py b/langfuse/api/scim/types/service_provider_config.py new file mode 100644 index 000000000..48add080e --- /dev/null +++ b/langfuse/api/scim/types/service_provider_config.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .authentication_scheme import AuthenticationScheme +from .bulk_config import BulkConfig +from .filter_config import FilterConfig +from .resource_meta import ResourceMeta +from .scim_feature_support import ScimFeatureSupport + + +class ServiceProviderConfig(UniversalBaseModel): + schemas: typing.List[str] + documentation_uri: typing_extensions.Annotated[ + str, FieldMetadata(alias="documentationUri") + ] + patch: ScimFeatureSupport + bulk: BulkConfig + filter: FilterConfig + change_password: typing_extensions.Annotated[ + ScimFeatureSupport, FieldMetadata(alias="changePassword") + ] + sort: ScimFeatureSupport + etag: ScimFeatureSupport + authentication_schemes: typing_extensions.Annotated[ + typing.List[AuthenticationScheme], FieldMetadata(alias="authenticationSchemes") + ] + meta: ResourceMeta + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scim/types/user_meta.py b/langfuse/api/scim/types/user_meta.py new file mode 100644 index 000000000..033ed4fa1 --- /dev/null +++ b/langfuse/api/scim/types/user_meta.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class UserMeta(UniversalBaseModel): + resource_type: typing_extensions.Annotated[str, FieldMetadata(alias="resourceType")] + created: typing.Optional[str] = None + last_modified: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="lastModified") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/score_configs/__init__.py b/langfuse/api/score_configs/__init__.py new file mode 100644 index 000000000..16f409522 --- /dev/null +++ b/langfuse/api/score_configs/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import CreateScoreConfigRequest, ScoreConfigs, UpdateScoreConfigRequest +_dynamic_imports: typing.Dict[str, str] = { + "CreateScoreConfigRequest": ".types", + "ScoreConfigs": ".types", + "UpdateScoreConfigRequest": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateScoreConfigRequest", "ScoreConfigs", "UpdateScoreConfigRequest"] diff --git a/langfuse/api/score_configs/client.py b/langfuse/api/score_configs/client.py new file mode 100644 index 000000000..b900e3f5d --- /dev/null +++ b/langfuse/api/score_configs/client.py @@ -0,0 +1,528 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..commons.types.config_category import ConfigCategory +from ..commons.types.score_config import ScoreConfig +from ..commons.types.score_config_data_type import ScoreConfigDataType +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawScoreConfigsClient, RawScoreConfigsClient +from .types.score_configs import ScoreConfigs + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ScoreConfigsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawScoreConfigsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawScoreConfigsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawScoreConfigsClient + """ + return self._raw_client + + def create( + self, + *, + name: str, + data_type: ScoreConfigDataType, + categories: typing.Optional[typing.Sequence[ConfigCategory]] = OMIT, + min_value: typing.Optional[float] = OMIT, + max_value: typing.Optional[float] = OMIT, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScoreConfig: + """ + Create a score configuration (config). Score configs are used to define the structure of scores + + Parameters + ---------- + name : str + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + + data_type : ScoreConfigDataType + + categories : typing.Optional[typing.Sequence[ConfigCategory]] + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + + min_value : typing.Optional[float] + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + + max_value : typing.Optional[float] + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + + description : typing.Optional[str] + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScoreConfig + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.commons import ScoreConfigDataType + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.score_configs.create( + name="name", + data_type=ScoreConfigDataType.NUMERIC, + ) + """ + _response = self._raw_client.create( + name=name, + data_type=data_type, + categories=categories, + min_value=min_value, + max_value=max_value, + description=description, + request_options=request_options, + ) + return _response.data + + def get( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScoreConfigs: + """ + Get all score configs + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScoreConfigs + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.score_configs.get() + """ + _response = self._raw_client.get( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + def get_by_id( + self, config_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ScoreConfig: + """ + Get a score config + + Parameters + ---------- + config_id : str + The unique langfuse identifier of a score config + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScoreConfig + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.score_configs.get_by_id( + config_id="configId", + ) + """ + _response = self._raw_client.get_by_id( + config_id, request_options=request_options + ) + return _response.data + + def update( + self, + config_id: str, + *, + is_archived: typing.Optional[bool] = OMIT, + name: typing.Optional[str] = OMIT, + categories: typing.Optional[typing.Sequence[ConfigCategory]] = OMIT, + min_value: typing.Optional[float] = OMIT, + max_value: typing.Optional[float] = OMIT, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScoreConfig: + """ + Update a score config + + Parameters + ---------- + config_id : str + The unique langfuse identifier of a score config + + is_archived : typing.Optional[bool] + The status of the score config showing if it is archived or not + + name : typing.Optional[str] + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + + categories : typing.Optional[typing.Sequence[ConfigCategory]] + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + + min_value : typing.Optional[float] + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + + max_value : typing.Optional[float] + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + + description : typing.Optional[str] + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScoreConfig + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.score_configs.update( + config_id="configId", + ) + """ + _response = self._raw_client.update( + config_id, + is_archived=is_archived, + name=name, + categories=categories, + min_value=min_value, + max_value=max_value, + description=description, + request_options=request_options, + ) + return _response.data + + +class AsyncScoreConfigsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawScoreConfigsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawScoreConfigsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawScoreConfigsClient + """ + return self._raw_client + + async def create( + self, + *, + name: str, + data_type: ScoreConfigDataType, + categories: typing.Optional[typing.Sequence[ConfigCategory]] = OMIT, + min_value: typing.Optional[float] = OMIT, + max_value: typing.Optional[float] = OMIT, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScoreConfig: + """ + Create a score configuration (config). Score configs are used to define the structure of scores + + Parameters + ---------- + name : str + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + + data_type : ScoreConfigDataType + + categories : typing.Optional[typing.Sequence[ConfigCategory]] + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + + min_value : typing.Optional[float] + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + + max_value : typing.Optional[float] + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + + description : typing.Optional[str] + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScoreConfig + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.commons import ScoreConfigDataType + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.score_configs.create( + name="name", + data_type=ScoreConfigDataType.NUMERIC, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + name=name, + data_type=data_type, + categories=categories, + min_value=min_value, + max_value=max_value, + description=description, + request_options=request_options, + ) + return _response.data + + async def get( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScoreConfigs: + """ + Get all score configs + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScoreConfigs + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.score_configs.get() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def get_by_id( + self, config_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ScoreConfig: + """ + Get a score config + + Parameters + ---------- + config_id : str + The unique langfuse identifier of a score config + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScoreConfig + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.score_configs.get_by_id( + config_id="configId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_by_id( + config_id, request_options=request_options + ) + return _response.data + + async def update( + self, + config_id: str, + *, + is_archived: typing.Optional[bool] = OMIT, + name: typing.Optional[str] = OMIT, + categories: typing.Optional[typing.Sequence[ConfigCategory]] = OMIT, + min_value: typing.Optional[float] = OMIT, + max_value: typing.Optional[float] = OMIT, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> ScoreConfig: + """ + Update a score config + + Parameters + ---------- + config_id : str + The unique langfuse identifier of a score config + + is_archived : typing.Optional[bool] + The status of the score config showing if it is archived or not + + name : typing.Optional[str] + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + + categories : typing.Optional[typing.Sequence[ConfigCategory]] + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + + min_value : typing.Optional[float] + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + + max_value : typing.Optional[float] + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + + description : typing.Optional[str] + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ScoreConfig + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.score_configs.update( + config_id="configId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + config_id, + is_archived=is_archived, + name=name, + categories=categories, + min_value=min_value, + max_value=max_value, + description=description, + request_options=request_options, + ) + return _response.data diff --git a/langfuse/api/score_configs/raw_client.py b/langfuse/api/score_configs/raw_client.py new file mode 100644 index 000000000..11de026c6 --- /dev/null +++ b/langfuse/api/score_configs/raw_client.py @@ -0,0 +1,1014 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.config_category import ConfigCategory +from ..commons.types.score_config import ScoreConfig +from ..commons.types.score_config_data_type import ScoreConfigDataType +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from .types.score_configs import ScoreConfigs + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawScoreConfigsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + name: str, + data_type: ScoreConfigDataType, + categories: typing.Optional[typing.Sequence[ConfigCategory]] = OMIT, + min_value: typing.Optional[float] = OMIT, + max_value: typing.Optional[float] = OMIT, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ScoreConfig]: + """ + Create a score configuration (config). Score configs are used to define the structure of scores + + Parameters + ---------- + name : str + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + + data_type : ScoreConfigDataType + + categories : typing.Optional[typing.Sequence[ConfigCategory]] + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + + min_value : typing.Optional[float] + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + + max_value : typing.Optional[float] + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + + description : typing.Optional[str] + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ScoreConfig] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/score-configs", + method="POST", + json={ + "name": name, + "dataType": data_type, + "categories": convert_and_respect_annotation_metadata( + object_=categories, + annotation=typing.Sequence[ConfigCategory], + direction="write", + ), + "minValue": min_value, + "maxValue": max_value, + "description": description, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScoreConfig, + parse_obj_as( + type_=ScoreConfig, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ScoreConfigs]: + """ + Get all score configs + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ScoreConfigs] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/score-configs", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScoreConfigs, + parse_obj_as( + type_=ScoreConfigs, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_by_id( + self, config_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ScoreConfig]: + """ + Get a score config + + Parameters + ---------- + config_id : str + The unique langfuse identifier of a score config + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ScoreConfig] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/score-configs/{jsonable_encoder(config_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScoreConfig, + parse_obj_as( + type_=ScoreConfig, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def update( + self, + config_id: str, + *, + is_archived: typing.Optional[bool] = OMIT, + name: typing.Optional[str] = OMIT, + categories: typing.Optional[typing.Sequence[ConfigCategory]] = OMIT, + min_value: typing.Optional[float] = OMIT, + max_value: typing.Optional[float] = OMIT, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ScoreConfig]: + """ + Update a score config + + Parameters + ---------- + config_id : str + The unique langfuse identifier of a score config + + is_archived : typing.Optional[bool] + The status of the score config showing if it is archived or not + + name : typing.Optional[str] + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + + categories : typing.Optional[typing.Sequence[ConfigCategory]] + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + + min_value : typing.Optional[float] + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + + max_value : typing.Optional[float] + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + + description : typing.Optional[str] + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ScoreConfig] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/score-configs/{jsonable_encoder(config_id)}", + method="PATCH", + json={ + "isArchived": is_archived, + "name": name, + "categories": convert_and_respect_annotation_metadata( + object_=categories, + annotation=typing.Sequence[ConfigCategory], + direction="write", + ), + "minValue": min_value, + "maxValue": max_value, + "description": description, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScoreConfig, + parse_obj_as( + type_=ScoreConfig, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawScoreConfigsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + name: str, + data_type: ScoreConfigDataType, + categories: typing.Optional[typing.Sequence[ConfigCategory]] = OMIT, + min_value: typing.Optional[float] = OMIT, + max_value: typing.Optional[float] = OMIT, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ScoreConfig]: + """ + Create a score configuration (config). Score configs are used to define the structure of scores + + Parameters + ---------- + name : str + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + + data_type : ScoreConfigDataType + + categories : typing.Optional[typing.Sequence[ConfigCategory]] + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + + min_value : typing.Optional[float] + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + + max_value : typing.Optional[float] + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + + description : typing.Optional[str] + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ScoreConfig] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/score-configs", + method="POST", + json={ + "name": name, + "dataType": data_type, + "categories": convert_and_respect_annotation_metadata( + object_=categories, + annotation=typing.Sequence[ConfigCategory], + direction="write", + ), + "minValue": min_value, + "maxValue": max_value, + "description": description, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScoreConfig, + parse_obj_as( + type_=ScoreConfig, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ScoreConfigs]: + """ + Get all score configs + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ScoreConfigs] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/score-configs", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScoreConfigs, + parse_obj_as( + type_=ScoreConfigs, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_by_id( + self, config_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ScoreConfig]: + """ + Get a score config + + Parameters + ---------- + config_id : str + The unique langfuse identifier of a score config + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ScoreConfig] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/score-configs/{jsonable_encoder(config_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScoreConfig, + parse_obj_as( + type_=ScoreConfig, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def update( + self, + config_id: str, + *, + is_archived: typing.Optional[bool] = OMIT, + name: typing.Optional[str] = OMIT, + categories: typing.Optional[typing.Sequence[ConfigCategory]] = OMIT, + min_value: typing.Optional[float] = OMIT, + max_value: typing.Optional[float] = OMIT, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ScoreConfig]: + """ + Update a score config + + Parameters + ---------- + config_id : str + The unique langfuse identifier of a score config + + is_archived : typing.Optional[bool] + The status of the score config showing if it is archived or not + + name : typing.Optional[str] + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + + categories : typing.Optional[typing.Sequence[ConfigCategory]] + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + + min_value : typing.Optional[float] + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + + max_value : typing.Optional[float] + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + + description : typing.Optional[str] + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ScoreConfig] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/score-configs/{jsonable_encoder(config_id)}", + method="PATCH", + json={ + "isArchived": is_archived, + "name": name, + "categories": convert_and_respect_annotation_metadata( + object_=categories, + annotation=typing.Sequence[ConfigCategory], + direction="write", + ), + "minValue": min_value, + "maxValue": max_value, + "description": description, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ScoreConfig, + parse_obj_as( + type_=ScoreConfig, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/score_configs/types/__init__.py b/langfuse/api/score_configs/types/__init__.py new file mode 100644 index 000000000..10ef4f679 --- /dev/null +++ b/langfuse/api/score_configs/types/__init__.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_score_config_request import CreateScoreConfigRequest + from .score_configs import ScoreConfigs + from .update_score_config_request import UpdateScoreConfigRequest +_dynamic_imports: typing.Dict[str, str] = { + "CreateScoreConfigRequest": ".create_score_config_request", + "ScoreConfigs": ".score_configs", + "UpdateScoreConfigRequest": ".update_score_config_request", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateScoreConfigRequest", "ScoreConfigs", "UpdateScoreConfigRequest"] diff --git a/langfuse/api/score_configs/types/create_score_config_request.py b/langfuse/api/score_configs/types/create_score_config_request.py new file mode 100644 index 000000000..0edb01407 --- /dev/null +++ b/langfuse/api/score_configs/types/create_score_config_request.py @@ -0,0 +1,50 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...commons.types.config_category import ConfigCategory +from ...commons.types.score_config_data_type import ScoreConfigDataType +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class CreateScoreConfigRequest(UniversalBaseModel): + name: str = pydantic.Field() + """ + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + """ + + data_type: typing_extensions.Annotated[ + ScoreConfigDataType, FieldMetadata(alias="dataType") + ] + categories: typing.Optional[typing.List[ConfigCategory]] = pydantic.Field( + default=None + ) + """ + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + """ + + min_value: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="minValue") + ] = pydantic.Field(default=None) + """ + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + """ + + max_value: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="maxValue") + ] = pydantic.Field(default=None) + """ + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/score_configs/types/score_configs.py b/langfuse/api/score_configs/types/score_configs.py new file mode 100644 index 000000000..d19763e9f --- /dev/null +++ b/langfuse/api/score_configs/types/score_configs.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.score_config import ScoreConfig +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse + + +class ScoreConfigs(UniversalBaseModel): + data: typing.List[ScoreConfig] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/score_configs/types/update_score_config_request.py b/langfuse/api/score_configs/types/update_score_config_request.py new file mode 100644 index 000000000..28c4248e9 --- /dev/null +++ b/langfuse/api/score_configs/types/update_score_config_request.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...commons.types.config_category import ConfigCategory +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class UpdateScoreConfigRequest(UniversalBaseModel): + is_archived: typing_extensions.Annotated[ + typing.Optional[bool], FieldMetadata(alias="isArchived") + ] = pydantic.Field(default=None) + """ + The status of the score config showing if it is archived or not + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Name of the score config. Max 35 characters. Only letters, numbers, underscores, spaces, periods, parentheses, and hyphens are allowed. + """ + + categories: typing.Optional[typing.List[ConfigCategory]] = pydantic.Field( + default=None + ) + """ + Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed + """ + + min_value: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="minValue") + ] = pydantic.Field(default=None) + """ + Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞ + """ + + max_value: typing_extensions.Annotated[ + typing.Optional[float], FieldMetadata(alias="maxValue") + ] = pydantic.Field(default=None) + """ + Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞ + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores/__init__.py b/langfuse/api/scores/__init__.py new file mode 100644 index 000000000..d57fb5371 --- /dev/null +++ b/langfuse/api/scores/__init__.py @@ -0,0 +1,82 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + GetScoresResponse, + GetScoresResponseData, + GetScoresResponseDataBoolean, + GetScoresResponseDataCategorical, + GetScoresResponseDataCorrection, + GetScoresResponseDataNumeric, + GetScoresResponseDataText, + GetScoresResponseData_Boolean, + GetScoresResponseData_Categorical, + GetScoresResponseData_Correction, + GetScoresResponseData_Numeric, + GetScoresResponseData_Text, + GetScoresResponseTraceData, + ) +_dynamic_imports: typing.Dict[str, str] = { + "GetScoresResponse": ".types", + "GetScoresResponseData": ".types", + "GetScoresResponseDataBoolean": ".types", + "GetScoresResponseDataCategorical": ".types", + "GetScoresResponseDataCorrection": ".types", + "GetScoresResponseDataNumeric": ".types", + "GetScoresResponseDataText": ".types", + "GetScoresResponseData_Boolean": ".types", + "GetScoresResponseData_Categorical": ".types", + "GetScoresResponseData_Correction": ".types", + "GetScoresResponseData_Numeric": ".types", + "GetScoresResponseData_Text": ".types", + "GetScoresResponseTraceData": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "GetScoresResponse", + "GetScoresResponseData", + "GetScoresResponseDataBoolean", + "GetScoresResponseDataCategorical", + "GetScoresResponseDataCorrection", + "GetScoresResponseDataNumeric", + "GetScoresResponseDataText", + "GetScoresResponseData_Boolean", + "GetScoresResponseData_Categorical", + "GetScoresResponseData_Correction", + "GetScoresResponseData_Numeric", + "GetScoresResponseData_Text", + "GetScoresResponseTraceData", +] diff --git a/langfuse/api/resources/score_v_2/client.py b/langfuse/api/scores/client.py similarity index 50% rename from langfuse/api/resources/score_v_2/client.py rename to langfuse/api/scores/client.py index 894b44f22..566530e21 100644 --- a/langfuse/api/resources/score_v_2/client.py +++ b/langfuse/api/scores/client.py @@ -2,30 +2,32 @@ import datetime as dt import typing -from json.decoder import JSONDecodeError - -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.datetime_utils import serialize_datetime -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import pydantic_v1 -from ...core.request_options import RequestOptions -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError + from ..commons.types.score import Score from ..commons.types.score_data_type import ScoreDataType from ..commons.types.score_source import ScoreSource +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawScoresClient, RawScoresClient from .types.get_scores_response import GetScoresResponse -class ScoreV2Client: +class ScoresClient: def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper + self._raw_client = RawScoresClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawScoresClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawScoresClient + """ + return self._raw_client - def get( + def get_many( self, *, page: typing.Optional[int] = None, @@ -40,9 +42,15 @@ def get( value: typing.Optional[float] = None, score_ids: typing.Optional[str] = None, config_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + dataset_run_id: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, queue_id: typing.Optional[str] = None, data_type: typing.Optional[ScoreDataType] = None, trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + fields: typing.Optional[str] = None, + filter: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> GetScoresResponse: """ @@ -54,7 +62,7 @@ def get( Page number, starts at 1. limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit. user_id : typing.Optional[str] Retrieve only scores with this userId associated to the trace. @@ -86,6 +94,18 @@ def get( config_id : typing.Optional[str] Retrieve only scores with a specific configId. + session_id : typing.Optional[str] + Retrieve only scores with a specific sessionId. + + dataset_run_id : typing.Optional[str] + Retrieve only scores with a specific datasetRunId. + + trace_id : typing.Optional[str] + Retrieve only scores with a specific traceId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter scores by. + queue_id : typing.Optional[str] Retrieve only scores with a specific annotation queueId. @@ -95,6 +115,12 @@ def get( trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] Only scores linked to traces that include all of these tags will be returned. + fields : typing.Optional[str] + Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment, sessionId). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned. + + filter : typing.Optional[str] + A JSON stringified array of filter objects. Each object requires type, column, operator, and value. Supports filtering by score metadata using the stringObject type. Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions. Supported operators for stringObject: =, contains, does not contain, starts with, ends with. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -104,9 +130,9 @@ def get( Examples -------- - from langfuse.client import FernLangfuse + from langfuse import LangfuseAPI - client = FernLangfuse( + client = LangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -114,59 +140,33 @@ def get( password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", ) - client.score_v_2.get() + client.scores.get_many() """ - _response = self._client_wrapper.httpx_client.request( - "api/public/v2/scores", - method="GET", - params={ - "page": page, - "limit": limit, - "userId": user_id, - "name": name, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "environment": environment, - "source": source, - "operator": operator, - "value": value, - "scoreIds": score_ids, - "configId": config_id, - "queueId": queue_id, - "dataType": data_type, - "traceTags": trace_tags, - }, + _response = self._raw_client.get_many( + page=page, + limit=limit, + user_id=user_id, + name=name, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + environment=environment, + source=source, + operator=operator, + value=value, + score_ids=score_ids, + config_id=config_id, + session_id=session_id, + dataset_run_id=dataset_run_id, + trace_id=trace_id, + observation_id=observation_id, + queue_id=queue_id, + data_type=data_type, + trace_tags=trace_tags, + fields=fields, + filter=filter, request_options=request_options, ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(GetScoresResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) + return _response.data def get_by_id( self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None @@ -188,9 +188,9 @@ def get_by_id( Examples -------- - from langfuse.client import FernLangfuse + from langfuse import LangfuseAPI - client = FernLangfuse( + client = LangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -198,47 +198,32 @@ def get_by_id( password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", ) - client.score_v_2.get_by_id( + client.scores.get_by_id( score_id="scoreId", ) """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/v2/scores/{jsonable_encoder(score_id)}", - method="GET", - request_options=request_options, + _response = self._raw_client.get_by_id( + score_id, request_options=request_options ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Score, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncScoreV2Client: + return _response.data + + +class AsyncScoresClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper + self._raw_client = AsyncRawScoresClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawScoresClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawScoresClient + """ + return self._raw_client - async def get( + async def get_many( self, *, page: typing.Optional[int] = None, @@ -253,9 +238,15 @@ async def get( value: typing.Optional[float] = None, score_ids: typing.Optional[str] = None, config_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + dataset_run_id: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, queue_id: typing.Optional[str] = None, data_type: typing.Optional[ScoreDataType] = None, trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + fields: typing.Optional[str] = None, + filter: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> GetScoresResponse: """ @@ -267,7 +258,7 @@ async def get( Page number, starts at 1. limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit. user_id : typing.Optional[str] Retrieve only scores with this userId associated to the trace. @@ -299,6 +290,18 @@ async def get( config_id : typing.Optional[str] Retrieve only scores with a specific configId. + session_id : typing.Optional[str] + Retrieve only scores with a specific sessionId. + + dataset_run_id : typing.Optional[str] + Retrieve only scores with a specific datasetRunId. + + trace_id : typing.Optional[str] + Retrieve only scores with a specific traceId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter scores by. + queue_id : typing.Optional[str] Retrieve only scores with a specific annotation queueId. @@ -308,6 +311,12 @@ async def get( trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] Only scores linked to traces that include all of these tags will be returned. + fields : typing.Optional[str] + Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment, sessionId). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned. + + filter : typing.Optional[str] + A JSON stringified array of filter objects. Each object requires type, column, operator, and value. Supports filtering by score metadata using the stringObject type. Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions. Supported operators for stringObject: =, contains, does not contain, starts with, ends with. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -319,9 +328,9 @@ async def get( -------- import asyncio - from langfuse.client import AsyncFernLangfuse + from langfuse import AsyncLangfuseAPI - client = AsyncFernLangfuse( + client = AsyncLangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -332,62 +341,36 @@ async def get( async def main() -> None: - await client.score_v_2.get() + await client.scores.get_many() asyncio.run(main()) """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/v2/scores", - method="GET", - params={ - "page": page, - "limit": limit, - "userId": user_id, - "name": name, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "environment": environment, - "source": source, - "operator": operator, - "value": value, - "scoreIds": score_ids, - "configId": config_id, - "queueId": queue_id, - "dataType": data_type, - "traceTags": trace_tags, - }, + _response = await self._raw_client.get_many( + page=page, + limit=limit, + user_id=user_id, + name=name, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + environment=environment, + source=source, + operator=operator, + value=value, + score_ids=score_ids, + config_id=config_id, + session_id=session_id, + dataset_run_id=dataset_run_id, + trace_id=trace_id, + observation_id=observation_id, + queue_id=queue_id, + data_type=data_type, + trace_tags=trace_tags, + fields=fields, + filter=filter, request_options=request_options, ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(GetScoresResponse, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) + return _response.data async def get_by_id( self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None @@ -411,9 +394,9 @@ async def get_by_id( -------- import asyncio - from langfuse.client import AsyncFernLangfuse + from langfuse import AsyncLangfuseAPI - client = AsyncFernLangfuse( + client = AsyncLangfuseAPI( x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", @@ -424,40 +407,14 @@ async def get_by_id( async def main() -> None: - await client.score_v_2.get_by_id( + await client.scores.get_by_id( score_id="scoreId", ) asyncio.run(main()) """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/v2/scores/{jsonable_encoder(score_id)}", - method="GET", - request_options=request_options, + _response = await self._raw_client.get_by_id( + score_id, request_options=request_options ) - try: - if 200 <= _response.status_code < 300: - return pydantic_v1.parse_obj_as(Score, _response.json()) # type: ignore - if _response.status_code == 400: - raise Error(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore - if _response.status_code == 401: - raise UnauthorizedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 403: - raise AccessDeniedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 405: - raise MethodNotAllowedError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - if _response.status_code == 404: - raise NotFoundError( - pydantic_v1.parse_obj_as(typing.Any, _response.json()) - ) # type: ignore - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) + return _response.data diff --git a/langfuse/api/scores/raw_client.py b/langfuse/api/scores/raw_client.py new file mode 100644 index 000000000..d1508545c --- /dev/null +++ b/langfuse/api/scores/raw_client.py @@ -0,0 +1,656 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.score import Score +from ..commons.types.score_data_type import ScoreDataType +from ..commons.types.score_source import ScoreSource +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.get_scores_response import GetScoresResponse + + +class RawScoresClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_many( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + user_id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + source: typing.Optional[ScoreSource] = None, + operator: typing.Optional[str] = None, + value: typing.Optional[float] = None, + score_ids: typing.Optional[str] = None, + config_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + dataset_run_id: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + data_type: typing.Optional[ScoreDataType] = None, + trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + fields: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetScoresResponse]: + """ + Get a list of scores (supports both trace and session scores) + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit. + + user_id : typing.Optional[str] + Retrieve only scores with this userId associated to the trace. + + name : typing.Optional[str] + Retrieve only scores with this name. + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include scores created on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include scores created before a certain datetime (ISO 8601) + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for scores where the environment is one of the provided values. + + source : typing.Optional[ScoreSource] + Retrieve only scores from a specific source. + + operator : typing.Optional[str] + Retrieve only scores with value. + + value : typing.Optional[float] + Retrieve only scores with value. + + score_ids : typing.Optional[str] + Comma-separated list of score IDs to limit the results to. + + config_id : typing.Optional[str] + Retrieve only scores with a specific configId. + + session_id : typing.Optional[str] + Retrieve only scores with a specific sessionId. + + dataset_run_id : typing.Optional[str] + Retrieve only scores with a specific datasetRunId. + + trace_id : typing.Optional[str] + Retrieve only scores with a specific traceId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter scores by. + + queue_id : typing.Optional[str] + Retrieve only scores with a specific annotation queueId. + + data_type : typing.Optional[ScoreDataType] + Retrieve only scores with a specific dataType. + + trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Only scores linked to traces that include all of these tags will be returned. + + fields : typing.Optional[str] + Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment, sessionId). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned. + + filter : typing.Optional[str] + A JSON stringified array of filter objects. Each object requires type, column, operator, and value. Supports filtering by score metadata using the stringObject type. Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions. Supported operators for stringObject: =, contains, does not contain, starts with, ends with. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetScoresResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/v2/scores", + method="GET", + params={ + "page": page, + "limit": limit, + "userId": user_id, + "name": name, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + "environment": environment, + "source": source, + "operator": operator, + "value": value, + "scoreIds": score_ids, + "configId": config_id, + "sessionId": session_id, + "datasetRunId": dataset_run_id, + "traceId": trace_id, + "observationId": observation_id, + "queueId": queue_id, + "dataType": data_type, + "traceTags": trace_tags, + "fields": fields, + "filter": filter, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScoresResponse, + parse_obj_as( + type_=GetScoresResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_by_id( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[Score]: + """ + Get a score (supports both trace and session scores) + + Parameters + ---------- + score_id : str + The unique langfuse identifier of a score + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Score] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/v2/scores/{jsonable_encoder(score_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Score, + parse_obj_as( + type_=Score, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawScoresClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_many( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + user_id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + source: typing.Optional[ScoreSource] = None, + operator: typing.Optional[str] = None, + value: typing.Optional[float] = None, + score_ids: typing.Optional[str] = None, + config_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + dataset_run_id: typing.Optional[str] = None, + trace_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + data_type: typing.Optional[ScoreDataType] = None, + trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + fields: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetScoresResponse]: + """ + Get a list of scores (supports both trace and session scores) + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1. + + limit : typing.Optional[int] + Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit. + + user_id : typing.Optional[str] + Retrieve only scores with this userId associated to the trace. + + name : typing.Optional[str] + Retrieve only scores with this name. + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include scores created on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include scores created before a certain datetime (ISO 8601) + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for scores where the environment is one of the provided values. + + source : typing.Optional[ScoreSource] + Retrieve only scores from a specific source. + + operator : typing.Optional[str] + Retrieve only scores with value. + + value : typing.Optional[float] + Retrieve only scores with value. + + score_ids : typing.Optional[str] + Comma-separated list of score IDs to limit the results to. + + config_id : typing.Optional[str] + Retrieve only scores with a specific configId. + + session_id : typing.Optional[str] + Retrieve only scores with a specific sessionId. + + dataset_run_id : typing.Optional[str] + Retrieve only scores with a specific datasetRunId. + + trace_id : typing.Optional[str] + Retrieve only scores with a specific traceId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter scores by. + + queue_id : typing.Optional[str] + Retrieve only scores with a specific annotation queueId. + + data_type : typing.Optional[ScoreDataType] + Retrieve only scores with a specific dataType. + + trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Only scores linked to traces that include all of these tags will be returned. + + fields : typing.Optional[str] + Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment, sessionId). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned. + + filter : typing.Optional[str] + A JSON stringified array of filter objects. Each object requires type, column, operator, and value. Supports filtering by score metadata using the stringObject type. Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions. Supported operators for stringObject: =, contains, does not contain, starts with, ends with. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetScoresResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/v2/scores", + method="GET", + params={ + "page": page, + "limit": limit, + "userId": user_id, + "name": name, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + "environment": environment, + "source": source, + "operator": operator, + "value": value, + "scoreIds": score_ids, + "configId": config_id, + "sessionId": session_id, + "datasetRunId": dataset_run_id, + "traceId": trace_id, + "observationId": observation_id, + "queueId": queue_id, + "dataType": data_type, + "traceTags": trace_tags, + "fields": fields, + "filter": filter, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScoresResponse, + parse_obj_as( + type_=GetScoresResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_by_id( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[Score]: + """ + Get a score (supports both trace and session scores) + + Parameters + ---------- + score_id : str + The unique langfuse identifier of a score + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Score] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/v2/scores/{jsonable_encoder(score_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Score, + parse_obj_as( + type_=Score, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/scores/types/__init__.py b/langfuse/api/scores/types/__init__.py new file mode 100644 index 000000000..5b82ed448 --- /dev/null +++ b/langfuse/api/scores/types/__init__.py @@ -0,0 +1,82 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .get_scores_response import GetScoresResponse + from .get_scores_response_data import ( + GetScoresResponseData, + GetScoresResponseData_Boolean, + GetScoresResponseData_Categorical, + GetScoresResponseData_Correction, + GetScoresResponseData_Numeric, + GetScoresResponseData_Text, + ) + from .get_scores_response_data_boolean import GetScoresResponseDataBoolean + from .get_scores_response_data_categorical import GetScoresResponseDataCategorical + from .get_scores_response_data_correction import GetScoresResponseDataCorrection + from .get_scores_response_data_numeric import GetScoresResponseDataNumeric + from .get_scores_response_data_text import GetScoresResponseDataText + from .get_scores_response_trace_data import GetScoresResponseTraceData +_dynamic_imports: typing.Dict[str, str] = { + "GetScoresResponse": ".get_scores_response", + "GetScoresResponseData": ".get_scores_response_data", + "GetScoresResponseDataBoolean": ".get_scores_response_data_boolean", + "GetScoresResponseDataCategorical": ".get_scores_response_data_categorical", + "GetScoresResponseDataCorrection": ".get_scores_response_data_correction", + "GetScoresResponseDataNumeric": ".get_scores_response_data_numeric", + "GetScoresResponseDataText": ".get_scores_response_data_text", + "GetScoresResponseData_Boolean": ".get_scores_response_data", + "GetScoresResponseData_Categorical": ".get_scores_response_data", + "GetScoresResponseData_Correction": ".get_scores_response_data", + "GetScoresResponseData_Numeric": ".get_scores_response_data", + "GetScoresResponseData_Text": ".get_scores_response_data", + "GetScoresResponseTraceData": ".get_scores_response_trace_data", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "GetScoresResponse", + "GetScoresResponseData", + "GetScoresResponseDataBoolean", + "GetScoresResponseDataCategorical", + "GetScoresResponseDataCorrection", + "GetScoresResponseDataNumeric", + "GetScoresResponseDataText", + "GetScoresResponseData_Boolean", + "GetScoresResponseData_Categorical", + "GetScoresResponseData_Correction", + "GetScoresResponseData_Numeric", + "GetScoresResponseData_Text", + "GetScoresResponseTraceData", +] diff --git a/langfuse/api/scores/types/get_scores_response.py b/langfuse/api/scores/types/get_scores_response.py new file mode 100644 index 000000000..0ca4d0e40 --- /dev/null +++ b/langfuse/api/scores/types/get_scores_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse +from .get_scores_response_data import GetScoresResponseData + + +class GetScoresResponse(UniversalBaseModel): + data: typing.List[GetScoresResponseData] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores/types/get_scores_response_data.py b/langfuse/api/scores/types/get_scores_response_data.py new file mode 100644 index 000000000..f85cd471c --- /dev/null +++ b/langfuse/api/scores/types/get_scores_response_data.py @@ -0,0 +1,258 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...commons.types.score_source import ScoreSource +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .get_scores_response_trace_data import GetScoresResponseTraceData + + +class GetScoresResponseData_Numeric(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["NUMERIC"], FieldMetadata(alias="dataType") + ] = "NUMERIC" + trace: typing.Optional[GetScoresResponseTraceData] = None + value: float + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class GetScoresResponseData_Categorical(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["CATEGORICAL"], FieldMetadata(alias="dataType") + ] = "CATEGORICAL" + trace: typing.Optional[GetScoresResponseTraceData] = None + value: float + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class GetScoresResponseData_Boolean(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["BOOLEAN"], FieldMetadata(alias="dataType") + ] = "BOOLEAN" + trace: typing.Optional[GetScoresResponseTraceData] = None + value: float + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class GetScoresResponseData_Correction(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["CORRECTION"], FieldMetadata(alias="dataType") + ] = "CORRECTION" + trace: typing.Optional[GetScoresResponseTraceData] = None + value: float + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class GetScoresResponseData_Text(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["TEXT"], FieldMetadata(alias="dataType") + ] = "TEXT" + trace: typing.Optional[GetScoresResponseTraceData] = None + string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = None + observation_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="observationId") + ] = None + dataset_run_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="datasetRunId") + ] = None + name: str + source: ScoreSource + timestamp: dt.datetime + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + comment: typing.Optional[str] = None + metadata: typing.Any + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + environment: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +GetScoresResponseData = typing_extensions.Annotated[ + typing.Union[ + GetScoresResponseData_Numeric, + GetScoresResponseData_Categorical, + GetScoresResponseData_Boolean, + GetScoresResponseData_Correction, + GetScoresResponseData_Text, + ], + pydantic.Field(discriminator="data_type"), +] diff --git a/langfuse/api/scores/types/get_scores_response_data_boolean.py b/langfuse/api/scores/types/get_scores_response_data_boolean.py new file mode 100644 index 000000000..eee645cd3 --- /dev/null +++ b/langfuse/api/scores/types/get_scores_response_data_boolean.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.boolean_score import BooleanScore +from .get_scores_response_trace_data import GetScoresResponseTraceData + + +class GetScoresResponseDataBoolean(BooleanScore): + trace: typing.Optional[GetScoresResponseTraceData] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores/types/get_scores_response_data_categorical.py b/langfuse/api/scores/types/get_scores_response_data_categorical.py new file mode 100644 index 000000000..edd4e46fa --- /dev/null +++ b/langfuse/api/scores/types/get_scores_response_data_categorical.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.categorical_score import CategoricalScore +from .get_scores_response_trace_data import GetScoresResponseTraceData + + +class GetScoresResponseDataCategorical(CategoricalScore): + trace: typing.Optional[GetScoresResponseTraceData] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores/types/get_scores_response_data_correction.py b/langfuse/api/scores/types/get_scores_response_data_correction.py new file mode 100644 index 000000000..df39c65ed --- /dev/null +++ b/langfuse/api/scores/types/get_scores_response_data_correction.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.correction_score import CorrectionScore +from .get_scores_response_trace_data import GetScoresResponseTraceData + + +class GetScoresResponseDataCorrection(CorrectionScore): + trace: typing.Optional[GetScoresResponseTraceData] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores/types/get_scores_response_data_numeric.py b/langfuse/api/scores/types/get_scores_response_data_numeric.py new file mode 100644 index 000000000..350be0e0c --- /dev/null +++ b/langfuse/api/scores/types/get_scores_response_data_numeric.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.numeric_score import NumericScore +from .get_scores_response_trace_data import GetScoresResponseTraceData + + +class GetScoresResponseDataNumeric(NumericScore): + trace: typing.Optional[GetScoresResponseTraceData] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores/types/get_scores_response_data_text.py b/langfuse/api/scores/types/get_scores_response_data_text.py new file mode 100644 index 000000000..4949afe74 --- /dev/null +++ b/langfuse/api/scores/types/get_scores_response_data_text.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.text_score import TextScore +from .get_scores_response_trace_data import GetScoresResponseTraceData + + +class GetScoresResponseDataText(TextScore): + trace: typing.Optional[GetScoresResponseTraceData] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores/types/get_scores_response_trace_data.py b/langfuse/api/scores/types/get_scores_response_trace_data.py new file mode 100644 index 000000000..306aaaf78 --- /dev/null +++ b/langfuse/api/scores/types/get_scores_response_trace_data.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class GetScoresResponseTraceData(UniversalBaseModel): + user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="userId") + ] = pydantic.Field(default=None) + """ + The user ID associated with the trace referenced by score + """ + + tags: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + A list of tags associated with the trace referenced by score + """ + + environment: typing.Optional[str] = pydantic.Field(default=None) + """ + The environment of the trace referenced by score + """ + + session_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="sessionId") + ] = pydantic.Field(default=None) + """ + The session ID associated with the trace referenced by score + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/__init__.py b/langfuse/api/scores_v3/__init__.py new file mode 100644 index 000000000..855868335 --- /dev/null +++ b/langfuse/api/scores_v3/__init__.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + BaseScoreV3, + BooleanScoreV3, + CategoricalScoreV3, + CorrectionScoreV3, + GetScoresV3Meta, + GetScoresV3Response, + NumericScoreV3, + ScoreSubjectExperimentV3, + ScoreSubjectObservationV3, + ScoreSubjectSessionV3, + ScoreSubjectTraceV3, + ScoreSubjectV3, + ScoreSubjectV3_Experiment, + ScoreSubjectV3_Observation, + ScoreSubjectV3_Session, + ScoreSubjectV3_Trace, + ScoreV3, + ScoreV3_Boolean, + ScoreV3_Categorical, + ScoreV3_Correction, + ScoreV3_Numeric, + ScoreV3_Text, + TextScoreV3, + ) +_dynamic_imports: typing.Dict[str, str] = { + "BaseScoreV3": ".types", + "BooleanScoreV3": ".types", + "CategoricalScoreV3": ".types", + "CorrectionScoreV3": ".types", + "GetScoresV3Meta": ".types", + "GetScoresV3Response": ".types", + "NumericScoreV3": ".types", + "ScoreSubjectExperimentV3": ".types", + "ScoreSubjectObservationV3": ".types", + "ScoreSubjectSessionV3": ".types", + "ScoreSubjectTraceV3": ".types", + "ScoreSubjectV3": ".types", + "ScoreSubjectV3_Experiment": ".types", + "ScoreSubjectV3_Observation": ".types", + "ScoreSubjectV3_Session": ".types", + "ScoreSubjectV3_Trace": ".types", + "ScoreV3": ".types", + "ScoreV3_Boolean": ".types", + "ScoreV3_Categorical": ".types", + "ScoreV3_Correction": ".types", + "ScoreV3_Numeric": ".types", + "ScoreV3_Text": ".types", + "TextScoreV3": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BaseScoreV3", + "BooleanScoreV3", + "CategoricalScoreV3", + "CorrectionScoreV3", + "GetScoresV3Meta", + "GetScoresV3Response", + "NumericScoreV3", + "ScoreSubjectExperimentV3", + "ScoreSubjectObservationV3", + "ScoreSubjectSessionV3", + "ScoreSubjectTraceV3", + "ScoreSubjectV3", + "ScoreSubjectV3_Experiment", + "ScoreSubjectV3_Observation", + "ScoreSubjectV3_Session", + "ScoreSubjectV3_Trace", + "ScoreV3", + "ScoreV3_Boolean", + "ScoreV3_Categorical", + "ScoreV3_Correction", + "ScoreV3_Numeric", + "ScoreV3_Text", + "TextScoreV3", +] diff --git a/langfuse/api/scores_v3/client.py b/langfuse/api/scores_v3/client.py new file mode 100644 index 000000000..2755d3e74 --- /dev/null +++ b/langfuse/api/scores_v3/client.py @@ -0,0 +1,341 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawScoresV3Client, RawScoresV3Client +from .types.get_scores_v3response import GetScoresV3Response + + +class ScoresV3Client: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawScoresV3Client(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawScoresV3Client: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawScoresV3Client + """ + return self._raw_client + + def get_many_v3( + self, + *, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + fields: typing.Optional[str] = None, + id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + source: typing.Optional[str] = None, + data_type: typing.Optional[str] = None, + environment: typing.Optional[str] = None, + config_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + value: typing.Optional[str] = None, + value_min: typing.Optional[float] = None, + value_max: typing.Optional[float] = None, + trace_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, + experiment_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetScoresV3Response: + """ + Get a list of scores with a polymorphic `value` field (v3). + + This endpoint requires Langfuse v4 or later. + + The `value` field type depends on `dataType`: + - `NUMERIC` → number + - `BOOLEAN` → boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` → string + + Use the `fields` parameter to include optional field groups beyond the + default `core`. Unknown group names return HTTP 400. + + Parameters + ---------- + limit : typing.Optional[int] + Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. + + cursor : typing.Optional[str] + URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. + + fields : typing.Optional[str] + Comma-separated field groups to include. Allowed: core, details, subject, annotation. Defaults to "core". Unknown names return HTTP 400. + + id : typing.Optional[str] + Comma-separated list of score IDs to filter by (OR within, AND across filters). + + name : typing.Optional[str] + Comma-separated list of score names to filter by. + + source : typing.Optional[str] + Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. + + data_type : typing.Optional[str] + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. + + environment : typing.Optional[str] + Comma-separated list of environments to filter by. + + config_id : typing.Optional[str] + Comma-separated list of score config IDs to filter by. + + queue_id : typing.Optional[str] + Comma-separated list of annotation queue IDs to filter by. + + author_user_id : typing.Optional[str] + Comma-separated list of author user IDs to filter by. + + value : typing.Optional[str] + Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. + + value_min : typing.Optional[float] + Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + value_max : typing.Optional[float] + Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + trace_id : typing.Optional[str] + Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. + + session_id : typing.Optional[str] + Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. + + experiment_id : typing.Optional[str] + Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. + + from_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on the score timestamp. + + to_timestamp : typing.Optional[dt.datetime] + Exclusive upper bound on the score timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetScoresV3Response + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scores_v3.get_many_v3() + """ + _response = self._raw_client.get_many_v3( + limit=limit, + cursor=cursor, + fields=fields, + id=id, + name=name, + source=source, + data_type=data_type, + environment=environment, + config_id=config_id, + queue_id=queue_id, + author_user_id=author_user_id, + value=value, + value_min=value_min, + value_max=value_max, + trace_id=trace_id, + session_id=session_id, + observation_id=observation_id, + experiment_id=experiment_id, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + request_options=request_options, + ) + return _response.data + + +class AsyncScoresV3Client: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawScoresV3Client(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawScoresV3Client: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawScoresV3Client + """ + return self._raw_client + + async def get_many_v3( + self, + *, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + fields: typing.Optional[str] = None, + id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + source: typing.Optional[str] = None, + data_type: typing.Optional[str] = None, + environment: typing.Optional[str] = None, + config_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + value: typing.Optional[str] = None, + value_min: typing.Optional[float] = None, + value_max: typing.Optional[float] = None, + trace_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, + experiment_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetScoresV3Response: + """ + Get a list of scores with a polymorphic `value` field (v3). + + This endpoint requires Langfuse v4 or later. + + The `value` field type depends on `dataType`: + - `NUMERIC` → number + - `BOOLEAN` → boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` → string + + Use the `fields` parameter to include optional field groups beyond the + default `core`. Unknown group names return HTTP 400. + + Parameters + ---------- + limit : typing.Optional[int] + Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. + + cursor : typing.Optional[str] + URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. + + fields : typing.Optional[str] + Comma-separated field groups to include. Allowed: core, details, subject, annotation. Defaults to "core". Unknown names return HTTP 400. + + id : typing.Optional[str] + Comma-separated list of score IDs to filter by (OR within, AND across filters). + + name : typing.Optional[str] + Comma-separated list of score names to filter by. + + source : typing.Optional[str] + Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. + + data_type : typing.Optional[str] + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. + + environment : typing.Optional[str] + Comma-separated list of environments to filter by. + + config_id : typing.Optional[str] + Comma-separated list of score config IDs to filter by. + + queue_id : typing.Optional[str] + Comma-separated list of annotation queue IDs to filter by. + + author_user_id : typing.Optional[str] + Comma-separated list of author user IDs to filter by. + + value : typing.Optional[str] + Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. + + value_min : typing.Optional[float] + Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + value_max : typing.Optional[float] + Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + trace_id : typing.Optional[str] + Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. + + session_id : typing.Optional[str] + Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. + + experiment_id : typing.Optional[str] + Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. + + from_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on the score timestamp. + + to_timestamp : typing.Optional[dt.datetime] + Exclusive upper bound on the score timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetScoresV3Response + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.scores_v3.get_many_v3() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_many_v3( + limit=limit, + cursor=cursor, + fields=fields, + id=id, + name=name, + source=source, + data_type=data_type, + environment=environment, + config_id=config_id, + queue_id=queue_id, + author_user_id=author_user_id, + value=value, + value_min=value_min, + value_max=value_max, + trace_id=trace_id, + session_id=session_id, + observation_id=observation_id, + experiment_id=experiment_id, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + request_options=request_options, + ) + return _response.data diff --git a/langfuse/api/scores_v3/raw_client.py b/langfuse/api/scores_v3/raw_client.py new file mode 100644 index 000000000..47c9f3f8d --- /dev/null +++ b/langfuse/api/scores_v3/raw_client.py @@ -0,0 +1,460 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.get_scores_v3response import GetScoresV3Response + + +class RawScoresV3Client: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_many_v3( + self, + *, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + fields: typing.Optional[str] = None, + id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + source: typing.Optional[str] = None, + data_type: typing.Optional[str] = None, + environment: typing.Optional[str] = None, + config_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + value: typing.Optional[str] = None, + value_min: typing.Optional[float] = None, + value_max: typing.Optional[float] = None, + trace_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, + experiment_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetScoresV3Response]: + """ + Get a list of scores with a polymorphic `value` field (v3). + + This endpoint requires Langfuse v4 or later. + + The `value` field type depends on `dataType`: + - `NUMERIC` → number + - `BOOLEAN` → boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` → string + + Use the `fields` parameter to include optional field groups beyond the + default `core`. Unknown group names return HTTP 400. + + Parameters + ---------- + limit : typing.Optional[int] + Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. + + cursor : typing.Optional[str] + URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. + + fields : typing.Optional[str] + Comma-separated field groups to include. Allowed: core, details, subject, annotation. Defaults to "core". Unknown names return HTTP 400. + + id : typing.Optional[str] + Comma-separated list of score IDs to filter by (OR within, AND across filters). + + name : typing.Optional[str] + Comma-separated list of score names to filter by. + + source : typing.Optional[str] + Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. + + data_type : typing.Optional[str] + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. + + environment : typing.Optional[str] + Comma-separated list of environments to filter by. + + config_id : typing.Optional[str] + Comma-separated list of score config IDs to filter by. + + queue_id : typing.Optional[str] + Comma-separated list of annotation queue IDs to filter by. + + author_user_id : typing.Optional[str] + Comma-separated list of author user IDs to filter by. + + value : typing.Optional[str] + Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. + + value_min : typing.Optional[float] + Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + value_max : typing.Optional[float] + Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + trace_id : typing.Optional[str] + Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. + + session_id : typing.Optional[str] + Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. + + experiment_id : typing.Optional[str] + Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. + + from_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on the score timestamp. + + to_timestamp : typing.Optional[dt.datetime] + Exclusive upper bound on the score timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetScoresV3Response] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/v3/scores", + method="GET", + params={ + "limit": limit, + "cursor": cursor, + "fields": fields, + "id": id, + "name": name, + "source": source, + "dataType": data_type, + "environment": environment, + "configId": config_id, + "queueId": queue_id, + "authorUserId": author_user_id, + "value": value, + "valueMin": value_min, + "valueMax": value_max, + "traceId": trace_id, + "sessionId": session_id, + "observationId": observation_id, + "experimentId": experiment_id, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScoresV3Response, + parse_obj_as( + type_=GetScoresV3Response, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawScoresV3Client: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_many_v3( + self, + *, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + fields: typing.Optional[str] = None, + id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + source: typing.Optional[str] = None, + data_type: typing.Optional[str] = None, + environment: typing.Optional[str] = None, + config_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + value: typing.Optional[str] = None, + value_min: typing.Optional[float] = None, + value_max: typing.Optional[float] = None, + trace_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, + experiment_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetScoresV3Response]: + """ + Get a list of scores with a polymorphic `value` field (v3). + + This endpoint requires Langfuse v4 or later. + + The `value` field type depends on `dataType`: + - `NUMERIC` → number + - `BOOLEAN` → boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` → string + + Use the `fields` parameter to include optional field groups beyond the + default `core`. Unknown group names return HTTP 400. + + Parameters + ---------- + limit : typing.Optional[int] + Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. + + cursor : typing.Optional[str] + URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. + + fields : typing.Optional[str] + Comma-separated field groups to include. Allowed: core, details, subject, annotation. Defaults to "core". Unknown names return HTTP 400. + + id : typing.Optional[str] + Comma-separated list of score IDs to filter by (OR within, AND across filters). + + name : typing.Optional[str] + Comma-separated list of score names to filter by. + + source : typing.Optional[str] + Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. + + data_type : typing.Optional[str] + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. + + environment : typing.Optional[str] + Comma-separated list of environments to filter by. + + config_id : typing.Optional[str] + Comma-separated list of score config IDs to filter by. + + queue_id : typing.Optional[str] + Comma-separated list of annotation queue IDs to filter by. + + author_user_id : typing.Optional[str] + Comma-separated list of author user IDs to filter by. + + value : typing.Optional[str] + Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. + + value_min : typing.Optional[float] + Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + value_max : typing.Optional[float] + Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + trace_id : typing.Optional[str] + Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. + + session_id : typing.Optional[str] + Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. + + experiment_id : typing.Optional[str] + Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. + + from_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on the score timestamp. + + to_timestamp : typing.Optional[dt.datetime] + Exclusive upper bound on the score timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetScoresV3Response] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/v3/scores", + method="GET", + params={ + "limit": limit, + "cursor": cursor, + "fields": fields, + "id": id, + "name": name, + "source": source, + "dataType": data_type, + "environment": environment, + "configId": config_id, + "queueId": queue_id, + "authorUserId": author_user_id, + "value": value, + "valueMin": value_min, + "valueMax": value_max, + "traceId": trace_id, + "sessionId": session_id, + "observationId": observation_id, + "experimentId": experiment_id, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetScoresV3Response, + parse_obj_as( + type_=GetScoresV3Response, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/scores_v3/types/__init__.py b/langfuse/api/scores_v3/types/__init__.py new file mode 100644 index 000000000..14da0ca73 --- /dev/null +++ b/langfuse/api/scores_v3/types/__init__.py @@ -0,0 +1,114 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .base_score_v3 import BaseScoreV3 + from .boolean_score_v3 import BooleanScoreV3 + from .categorical_score_v3 import CategoricalScoreV3 + from .correction_score_v3 import CorrectionScoreV3 + from .get_scores_v3meta import GetScoresV3Meta + from .get_scores_v3response import GetScoresV3Response + from .numeric_score_v3 import NumericScoreV3 + from .score_subject_experiment_v3 import ScoreSubjectExperimentV3 + from .score_subject_observation_v3 import ScoreSubjectObservationV3 + from .score_subject_session_v3 import ScoreSubjectSessionV3 + from .score_subject_trace_v3 import ScoreSubjectTraceV3 + from .score_subject_v3 import ( + ScoreSubjectV3, + ScoreSubjectV3_Experiment, + ScoreSubjectV3_Observation, + ScoreSubjectV3_Session, + ScoreSubjectV3_Trace, + ) + from .score_v3 import ( + ScoreV3, + ScoreV3_Boolean, + ScoreV3_Categorical, + ScoreV3_Correction, + ScoreV3_Numeric, + ScoreV3_Text, + ) + from .text_score_v3 import TextScoreV3 +_dynamic_imports: typing.Dict[str, str] = { + "BaseScoreV3": ".base_score_v3", + "BooleanScoreV3": ".boolean_score_v3", + "CategoricalScoreV3": ".categorical_score_v3", + "CorrectionScoreV3": ".correction_score_v3", + "GetScoresV3Meta": ".get_scores_v3meta", + "GetScoresV3Response": ".get_scores_v3response", + "NumericScoreV3": ".numeric_score_v3", + "ScoreSubjectExperimentV3": ".score_subject_experiment_v3", + "ScoreSubjectObservationV3": ".score_subject_observation_v3", + "ScoreSubjectSessionV3": ".score_subject_session_v3", + "ScoreSubjectTraceV3": ".score_subject_trace_v3", + "ScoreSubjectV3": ".score_subject_v3", + "ScoreSubjectV3_Experiment": ".score_subject_v3", + "ScoreSubjectV3_Observation": ".score_subject_v3", + "ScoreSubjectV3_Session": ".score_subject_v3", + "ScoreSubjectV3_Trace": ".score_subject_v3", + "ScoreV3": ".score_v3", + "ScoreV3_Boolean": ".score_v3", + "ScoreV3_Categorical": ".score_v3", + "ScoreV3_Correction": ".score_v3", + "ScoreV3_Numeric": ".score_v3", + "ScoreV3_Text": ".score_v3", + "TextScoreV3": ".text_score_v3", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BaseScoreV3", + "BooleanScoreV3", + "CategoricalScoreV3", + "CorrectionScoreV3", + "GetScoresV3Meta", + "GetScoresV3Response", + "NumericScoreV3", + "ScoreSubjectExperimentV3", + "ScoreSubjectObservationV3", + "ScoreSubjectSessionV3", + "ScoreSubjectTraceV3", + "ScoreSubjectV3", + "ScoreSubjectV3_Experiment", + "ScoreSubjectV3_Observation", + "ScoreSubjectV3_Session", + "ScoreSubjectV3_Trace", + "ScoreV3", + "ScoreV3_Boolean", + "ScoreV3_Categorical", + "ScoreV3_Correction", + "ScoreV3_Numeric", + "ScoreV3_Text", + "TextScoreV3", +] diff --git a/langfuse/api/scores_v3/types/base_score_v3.py b/langfuse/api/scores_v3/types/base_score_v3.py new file mode 100644 index 000000000..3d5394f95 --- /dev/null +++ b/langfuse/api/scores_v3/types/base_score_v3.py @@ -0,0 +1,71 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...commons.types.score_source import ScoreSource +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .score_subject_v3 import ScoreSubjectV3 + + +class BaseScoreV3(UniversalBaseModel): + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + name: str + source: ScoreSource + timestamp: dt.datetime + environment: str = pydantic.Field() + """ + The environment from which this score originated. + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + comment: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional comment attached to the score. Present when "details" is included in the fields parameter. + """ + + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = pydantic.Field(default=None) + """ + The score config ID, if this score was created from a config. Present when "details" is included in the fields parameter. + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field( + default=None + ) + """ + Arbitrary metadata attached to the score. Present when "details" is included in the fields parameter. + """ + + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = pydantic.Field(default=None) + """ + The user who created this score, if available. Present when "annotation" is included in the fields parameter. + """ + + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = pydantic.Field(default=None) + """ + The annotation queue this score belongs to, if any. Present when "annotation" is included in the fields parameter. + """ + + subject: typing.Optional[ScoreSubjectV3] = pydantic.Field(default=None) + """ + The entity this score is attached to (trace, observation, session, or experiment). Present when "subject" is included in the fields parameter. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/boolean_score_v3.py b/langfuse/api/scores_v3/types/boolean_score_v3.py new file mode 100644 index 000000000..5b94bc1d1 --- /dev/null +++ b/langfuse/api/scores_v3/types/boolean_score_v3.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_score_v3 import BaseScoreV3 + + +class BooleanScoreV3(BaseScoreV3): + value: bool = pydantic.Field() + """ + The boolean value of the score. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/categorical_score_v3.py b/langfuse/api/scores_v3/types/categorical_score_v3.py new file mode 100644 index 000000000..975b1f64c --- /dev/null +++ b/langfuse/api/scores_v3/types/categorical_score_v3.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_score_v3 import BaseScoreV3 + + +class CategoricalScoreV3(BaseScoreV3): + value: str = pydantic.Field() + """ + The string category value of the score. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/correction_score_v3.py b/langfuse/api/scores_v3/types/correction_score_v3.py new file mode 100644 index 000000000..1717a6e67 --- /dev/null +++ b/langfuse/api/scores_v3/types/correction_score_v3.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_score_v3 import BaseScoreV3 + + +class CorrectionScoreV3(BaseScoreV3): + value: str = pydantic.Field() + """ + The correction content of the score. Empty string if not set. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/get_scores_v3meta.py b/langfuse/api/scores_v3/types/get_scores_v3meta.py new file mode 100644 index 000000000..7dfcfe0e1 --- /dev/null +++ b/langfuse/api/scores_v3/types/get_scores_v3meta.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class GetScoresV3Meta(UniversalBaseModel): + limit: int + cursor: typing.Optional[str] = pydantic.Field(default=None) + """ + URL-safe base64 (base64url) cursor for the next page. Absent when there are no more results. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/get_scores_v3response.py b/langfuse/api/scores_v3/types/get_scores_v3response.py new file mode 100644 index 000000000..4d625b29a --- /dev/null +++ b/langfuse/api/scores_v3/types/get_scores_v3response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel +from .get_scores_v3meta import GetScoresV3Meta +from .score_v3 import ScoreV3 + + +class GetScoresV3Response(UniversalBaseModel): + data: typing.List[ScoreV3] + meta: GetScoresV3Meta + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/numeric_score_v3.py b/langfuse/api/scores_v3/types/numeric_score_v3.py new file mode 100644 index 000000000..10df001a4 --- /dev/null +++ b/langfuse/api/scores_v3/types/numeric_score_v3.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_score_v3 import BaseScoreV3 + + +class NumericScoreV3(BaseScoreV3): + value: float = pydantic.Field() + """ + The numeric value of the score. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/score_subject_experiment_v3.py b/langfuse/api/scores_v3/types/score_subject_experiment_v3.py new file mode 100644 index 000000000..a71a49241 --- /dev/null +++ b/langfuse/api/scores_v3/types/score_subject_experiment_v3.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ScoreSubjectExperimentV3(UniversalBaseModel): + id: str = pydantic.Field() + """ + The dataset run ID (experiment ID). + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/score_subject_observation_v3.py b/langfuse/api/scores_v3/types/score_subject_observation_v3.py new file mode 100644 index 000000000..1bc2edf20 --- /dev/null +++ b/langfuse/api/scores_v3/types/score_subject_observation_v3.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class ScoreSubjectObservationV3(UniversalBaseModel): + id: str = pydantic.Field() + """ + The observation ID. + """ + + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = pydantic.Field(default=None) + """ + The parent trace ID, if available. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/score_subject_session_v3.py b/langfuse/api/scores_v3/types/score_subject_session_v3.py new file mode 100644 index 000000000..cb9347583 --- /dev/null +++ b/langfuse/api/scores_v3/types/score_subject_session_v3.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ScoreSubjectSessionV3(UniversalBaseModel): + id: str = pydantic.Field() + """ + The session ID. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/score_subject_trace_v3.py b/langfuse/api/scores_v3/types/score_subject_trace_v3.py new file mode 100644 index 000000000..26aab7f07 --- /dev/null +++ b/langfuse/api/scores_v3/types/score_subject_trace_v3.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class ScoreSubjectTraceV3(UniversalBaseModel): + id: str = pydantic.Field() + """ + The trace ID. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/scores_v3/types/score_subject_v3.py b/langfuse/api/scores_v3/types/score_subject_v3.py new file mode 100644 index 000000000..7464fda55 --- /dev/null +++ b/langfuse/api/scores_v3/types/score_subject_v3.py @@ -0,0 +1,76 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata + + +class ScoreSubjectV3_Trace(UniversalBaseModel): + """ + A reference to the entity this score is attached to. Discriminated by "kind" — one of trace, observation, session, or experiment. + """ + + kind: typing.Literal["trace"] = "trace" + id: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreSubjectV3_Observation(UniversalBaseModel): + """ + A reference to the entity this score is attached to. Discriminated by "kind" — one of trace, observation, session, or experiment. + """ + + kind: typing.Literal["observation"] = "observation" + id: str + trace_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="traceId") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreSubjectV3_Session(UniversalBaseModel): + """ + A reference to the entity this score is attached to. Discriminated by "kind" — one of trace, observation, session, or experiment. + """ + + kind: typing.Literal["session"] = "session" + id: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreSubjectV3_Experiment(UniversalBaseModel): + """ + A reference to the entity this score is attached to. Discriminated by "kind" — one of trace, observation, session, or experiment. + """ + + kind: typing.Literal["experiment"] = "experiment" + id: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +ScoreSubjectV3 = typing_extensions.Annotated[ + typing.Union[ + ScoreSubjectV3_Trace, + ScoreSubjectV3_Observation, + ScoreSubjectV3_Session, + ScoreSubjectV3_Experiment, + ], + pydantic.Field(discriminator="kind"), +] diff --git a/langfuse/api/scores_v3/types/score_v3.py b/langfuse/api/scores_v3/types/score_v3.py new file mode 100644 index 000000000..9921d1bda --- /dev/null +++ b/langfuse/api/scores_v3/types/score_v3.py @@ -0,0 +1,200 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...commons.types.score_source import ScoreSource +from ...core.pydantic_utilities import UniversalBaseModel +from ...core.serialization import FieldMetadata +from .score_subject_v3 import ScoreSubjectV3 + + +class ScoreV3_Numeric(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["NUMERIC"], FieldMetadata(alias="dataType") + ] = "NUMERIC" + value: float + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + name: str + source: ScoreSource + timestamp: dt.datetime + environment: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + comment: typing.Optional[str] = None + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + metadata: typing.Optional[typing.Dict[str, typing.Any]] = None + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + subject: typing.Optional[ScoreSubjectV3] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreV3_Boolean(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["BOOLEAN"], FieldMetadata(alias="dataType") + ] = "BOOLEAN" + value: bool + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + name: str + source: ScoreSource + timestamp: dt.datetime + environment: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + comment: typing.Optional[str] = None + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + metadata: typing.Optional[typing.Dict[str, typing.Any]] = None + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + subject: typing.Optional[ScoreSubjectV3] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreV3_Categorical(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["CATEGORICAL"], FieldMetadata(alias="dataType") + ] = "CATEGORICAL" + value: str + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + name: str + source: ScoreSource + timestamp: dt.datetime + environment: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + comment: typing.Optional[str] = None + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + metadata: typing.Optional[typing.Dict[str, typing.Any]] = None + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + subject: typing.Optional[ScoreSubjectV3] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreV3_Text(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["TEXT"], FieldMetadata(alias="dataType") + ] = "TEXT" + value: str + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + name: str + source: ScoreSource + timestamp: dt.datetime + environment: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + comment: typing.Optional[str] = None + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + metadata: typing.Optional[typing.Dict[str, typing.Any]] = None + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + subject: typing.Optional[ScoreSubjectV3] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class ScoreV3_Correction(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + typing.Literal["CORRECTION"], FieldMetadata(alias="dataType") + ] = "CORRECTION" + value: str + id: str + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + name: str + source: ScoreSource + timestamp: dt.datetime + environment: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + comment: typing.Optional[str] = None + config_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="configId") + ] = None + metadata: typing.Optional[typing.Dict[str, typing.Any]] = None + author_user_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="authorUserId") + ] = None + queue_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queueId") + ] = None + subject: typing.Optional[ScoreSubjectV3] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +ScoreV3 = typing_extensions.Annotated[ + typing.Union[ + ScoreV3_Numeric, + ScoreV3_Boolean, + ScoreV3_Categorical, + ScoreV3_Text, + ScoreV3_Correction, + ], + pydantic.Field(discriminator="data_type"), +] diff --git a/langfuse/api/scores_v3/types/text_score_v3.py b/langfuse/api/scores_v3/types/text_score_v3.py new file mode 100644 index 000000000..3d658972c --- /dev/null +++ b/langfuse/api/scores_v3/types/text_score_v3.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from .base_score_v3 import BaseScoreV3 + + +class TextScoreV3(BaseScoreV3): + value: str = pydantic.Field() + """ + The text content of the score. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/sessions/__init__.py b/langfuse/api/sessions/__init__.py new file mode 100644 index 000000000..89b7e63bc --- /dev/null +++ b/langfuse/api/sessions/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import PaginatedSessions +_dynamic_imports: typing.Dict[str, str] = {"PaginatedSessions": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["PaginatedSessions"] diff --git a/langfuse/api/sessions/client.py b/langfuse/api/sessions/client.py new file mode 100644 index 000000000..b99829feb --- /dev/null +++ b/langfuse/api/sessions/client.py @@ -0,0 +1,262 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..commons.types.session_with_traces import SessionWithTraces +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawSessionsClient, RawSessionsClient +from .types.paginated_sessions import PaginatedSessions + + +class SessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSessionsClient + """ + return self._raw_client + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedSessions: + """ + Get sessions + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1 + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include sessions created on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include sessions created before a certain datetime (ISO 8601) + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for sessions where the environment is one of the provided values. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedSessions + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.list() + """ + _response = self._raw_client.list( + page=page, + limit=limit, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + environment=environment, + request_options=request_options, + ) + return _response.data + + def get( + self, + session_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionWithTraces: + """ + Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=` + + Parameters + ---------- + session_id : str + The unique id of a session + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionWithTraces + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.get( + session_id="sessionId", + ) + """ + _response = self._raw_client.get(session_id, request_options=request_options) + return _response.data + + +class AsyncSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSessionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSessionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSessionsClient + """ + return self._raw_client + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> PaginatedSessions: + """ + Get sessions + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1 + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include sessions created on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include sessions created before a certain datetime (ISO 8601) + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for sessions where the environment is one of the provided values. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + PaginatedSessions + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + page=page, + limit=limit, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + environment=environment, + request_options=request_options, + ) + return _response.data + + async def get( + self, + session_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> SessionWithTraces: + """ + Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=` + + Parameters + ---------- + session_id : str + The unique id of a session + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SessionWithTraces + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.get( + session_id="sessionId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + session_id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/sessions/raw_client.py b/langfuse/api/sessions/raw_client.py new file mode 100644 index 000000000..9c39a9a5a --- /dev/null +++ b/langfuse/api/sessions/raw_client.py @@ -0,0 +1,500 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.session_with_traces import SessionWithTraces +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.paginated_sessions import PaginatedSessions + + +class RawSessionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[PaginatedSessions]: + """ + Get sessions + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1 + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include sessions created on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include sessions created before a certain datetime (ISO 8601) + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for sessions where the environment is one of the provided values. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[PaginatedSessions] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/sessions", + method="GET", + params={ + "page": page, + "limit": limit, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + "environment": environment, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedSessions, + parse_obj_as( + type_=PaginatedSessions, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, + session_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SessionWithTraces]: + """ + Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=` + + Parameters + ---------- + session_id : str + The unique id of a session + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SessionWithTraces] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/sessions/{jsonable_encoder(session_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionWithTraces, + parse_obj_as( + type_=SessionWithTraces, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawSessionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[PaginatedSessions]: + """ + Get sessions + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1 + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include sessions created on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include sessions created before a certain datetime (ISO 8601) + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for sessions where the environment is one of the provided values. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[PaginatedSessions] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/sessions", + method="GET", + params={ + "page": page, + "limit": limit, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + "environment": environment, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + PaginatedSessions, + parse_obj_as( + type_=PaginatedSessions, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, + session_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SessionWithTraces]: + """ + Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=` + + Parameters + ---------- + session_id : str + The unique id of a session + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SessionWithTraces] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/sessions/{jsonable_encoder(session_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SessionWithTraces, + parse_obj_as( + type_=SessionWithTraces, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/sessions/types/__init__.py b/langfuse/api/sessions/types/__init__.py new file mode 100644 index 000000000..45f21e139 --- /dev/null +++ b/langfuse/api/sessions/types/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .paginated_sessions import PaginatedSessions +_dynamic_imports: typing.Dict[str, str] = {"PaginatedSessions": ".paginated_sessions"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["PaginatedSessions"] diff --git a/langfuse/api/sessions/types/paginated_sessions.py b/langfuse/api/sessions/types/paginated_sessions.py new file mode 100644 index 000000000..5d7bd3886 --- /dev/null +++ b/langfuse/api/sessions/types/paginated_sessions.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.session import Session +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse + + +class PaginatedSessions(UniversalBaseModel): + data: typing.List[Session] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/tests/utils/test_http_client.py b/langfuse/api/tests/utils/test_http_client.py deleted file mode 100644 index 950fcdeb1..000000000 --- a/langfuse/api/tests/utils/test_http_client.py +++ /dev/null @@ -1,59 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from langfuse.api.core.http_client import get_request_body -from langfuse.api.core.request_options import RequestOptions - - -def get_request_options() -> RequestOptions: - return {"additional_body_parameters": {"see you": "later"}} - - -def test_get_json_request_body() -> None: - json_body, data_body = get_request_body( - json={"hello": "world"}, data=None, request_options=None, omit=None - ) - assert json_body == {"hello": "world"} - assert data_body is None - - json_body_extras, data_body_extras = get_request_body( - json={"goodbye": "world"}, - data=None, - request_options=get_request_options(), - omit=None, - ) - - assert json_body_extras == {"goodbye": "world", "see you": "later"} - assert data_body_extras is None - - -def test_get_files_request_body() -> None: - json_body, data_body = get_request_body( - json=None, data={"hello": "world"}, request_options=None, omit=None - ) - assert data_body == {"hello": "world"} - assert json_body is None - - json_body_extras, data_body_extras = get_request_body( - json=None, - data={"goodbye": "world"}, - request_options=get_request_options(), - omit=None, - ) - - assert data_body_extras == {"goodbye": "world", "see you": "later"} - assert json_body_extras is None - - -def test_get_none_request_body() -> None: - json_body, data_body = get_request_body( - json=None, data=None, request_options=None, omit=None - ) - assert data_body is None - assert json_body is None - - json_body_extras, data_body_extras = get_request_body( - json=None, data=None, request_options=get_request_options(), omit=None - ) - - assert json_body_extras == {"see you": "later"} - assert data_body_extras is None diff --git a/langfuse/api/tests/utils/test_query_encoding.py b/langfuse/api/tests/utils/test_query_encoding.py deleted file mode 100644 index 9afa0ea78..000000000 --- a/langfuse/api/tests/utils/test_query_encoding.py +++ /dev/null @@ -1,19 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from langfuse.api.core.query_encoder import encode_query - - -def test_query_encoding() -> None: - assert encode_query({"hello world": "hello world"}) == { - "hello world": "hello world" - } - assert encode_query({"hello_world": {"hello": "world"}}) == { - "hello_world[hello]": "world" - } - assert encode_query( - {"hello_world": {"hello": {"world": "today"}, "test": "this"}, "hi": "there"} - ) == { - "hello_world[hello][world]": "today", - "hello_world[test]": "this", - "hi": "there", - } diff --git a/langfuse/api/trace/__init__.py b/langfuse/api/trace/__init__.py new file mode 100644 index 000000000..fc9889dfa --- /dev/null +++ b/langfuse/api/trace/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import DeleteTraceResponse, Sort, Traces +_dynamic_imports: typing.Dict[str, str] = { + "DeleteTraceResponse": ".types", + "Sort": ".types", + "Traces": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["DeleteTraceResponse", "Sort", "Traces"] diff --git a/langfuse/api/trace/client.py b/langfuse/api/trace/client.py new file mode 100644 index 000000000..d3d7b760a --- /dev/null +++ b/langfuse/api/trace/client.py @@ -0,0 +1,744 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..commons.types.trace_with_full_details import TraceWithFullDetails +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawTraceClient, RawTraceClient +from .types.delete_trace_response import DeleteTraceResponse +from .types.traces import Traces + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class TraceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawTraceClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawTraceClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawTraceClient + """ + return self._raw_client + + def get( + self, + trace_id: str, + *, + fields: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TraceWithFullDetails: + """ + Get a specific trace + + Parameters + ---------- + trace_id : str + The unique langfuse identifier of a trace + + fields : typing.Optional[str] + Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TraceWithFullDetails + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.trace.get( + trace_id="traceId", + ) + """ + _response = self._raw_client.get( + trace_id, fields=fields, request_options=request_options + ) + return _response.data + + def delete( + self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteTraceResponse: + """ + Delete a specific trace + + Parameters + ---------- + trace_id : str + The unique langfuse identifier of the trace to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteTraceResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.trace.delete( + trace_id="traceId", + ) + """ + _response = self._raw_client.delete(trace_id, request_options=request_options) + return _response.data + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + user_id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + order_by: typing.Optional[str] = None, + tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + version: typing.Optional[str] = None, + release: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + fields: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> Traces: + """ + Get list of traces + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1 + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + user_id : typing.Optional[str] + + name : typing.Optional[str] + + session_id : typing.Optional[str] + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) + + order_by : typing.Optional[str] + Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc + + tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Only traces that include all of these tags will be returned. + + version : typing.Optional[str] + Optional filter to only include traces with a certain version. + + release : typing.Optional[str] + Optional filter to only include traces with a certain release. + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for traces where the environment is one of the provided values. + + fields : typing.Optional[str] + Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, sessionId, tags, version, release, environment, fromTimestamp, toTimestamp). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Trace Fields + - `id` (string) - Trace ID + - `name` (string) - Trace name + - `timestamp` (datetime) - Trace timestamp + - `userId` (string) - User ID + - `sessionId` (string) - Session ID + - `environment` (string) - Environment tag + - `version` (string) - Version tag + - `release` (string) - Release tag + - `tags` (arrayOptions) - Array of tags + - `bookmarked` (boolean) - Bookmark status + + ### Structured Data + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + ### Aggregated Metrics (from observations) + These metrics are aggregated from all observations within the trace: + - `latency` (number) - Latency in seconds (time from first observation start to last observation end) + - `inputTokens` (number) - Total input tokens across all observations + - `outputTokens` (number) - Total output tokens across all observations + - `totalTokens` (number) - Total tokens (alias: `tokens`) + - `inputCost` (number) - Total input cost in USD + - `outputCost` (number) - Total output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Observation Level Aggregations + These fields aggregate observation levels within the trace: + - `level` (string) - Highest severity level (ERROR > WARNING > DEFAULT > DEBUG) + - `warningCount` (number) - Count of WARNING level observations + - `errorCount` (number) - Count of ERROR level observations + - `defaultCount` (number) - Count of DEFAULT level observations + - `debugCount` (number) - Count of DEBUG level observations + + ### Scores (requires join with scores table) + - `scores_avg` (number) - Average of numeric scores (alias: `scores`) + - `score_categories` (categoryOptions) - Categorical score values + + ## Filter Examples + ```json + [ + { + "type": "datetime", + "column": "timestamp", + "operator": ">=", + "value": "2024-01-01T00:00:00Z" + }, + { + "type": "string", + "column": "userId", + "operator": "=", + "value": "user-123" + }, + { + "type": "number", + "column": "totalCost", + "operator": ">=", + "value": 0.01 + }, + { + "type": "arrayOptions", + "column": "tags", + "operator": "all of", + "value": ["production", "critical"] + }, + { + "type": "stringObject", + "column": "metadata", + "key": "customer_tier", + "operator": "=", + "value": "enterprise" + } + ] + ``` + + ## Performance Notes + - Filtering on `userId`, `sessionId`, or `metadata` may enable skip indexes for better query performance + - Score filters require a join with the scores table and may impact query performance + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Traces + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.trace.list() + """ + _response = self._raw_client.list( + page=page, + limit=limit, + user_id=user_id, + name=name, + session_id=session_id, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + order_by=order_by, + tags=tags, + version=version, + release=release, + environment=environment, + fields=fields, + filter=filter, + request_options=request_options, + ) + return _response.data + + def delete_multiple( + self, + *, + trace_ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteTraceResponse: + """ + Delete multiple traces + + Parameters + ---------- + trace_ids : typing.Sequence[str] + List of trace IDs to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteTraceResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.trace.delete_multiple( + trace_ids=["traceIds", "traceIds"], + ) + """ + _response = self._raw_client.delete_multiple( + trace_ids=trace_ids, request_options=request_options + ) + return _response.data + + +class AsyncTraceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawTraceClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawTraceClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawTraceClient + """ + return self._raw_client + + async def get( + self, + trace_id: str, + *, + fields: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TraceWithFullDetails: + """ + Get a specific trace + + Parameters + ---------- + trace_id : str + The unique langfuse identifier of a trace + + fields : typing.Optional[str] + Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TraceWithFullDetails + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.trace.get( + trace_id="traceId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + trace_id, fields=fields, request_options=request_options + ) + return _response.data + + async def delete( + self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteTraceResponse: + """ + Delete a specific trace + + Parameters + ---------- + trace_id : str + The unique langfuse identifier of the trace to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteTraceResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.trace.delete( + trace_id="traceId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete( + trace_id, request_options=request_options + ) + return _response.data + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + user_id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + order_by: typing.Optional[str] = None, + tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + version: typing.Optional[str] = None, + release: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + fields: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> Traces: + """ + Get list of traces + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1 + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + user_id : typing.Optional[str] + + name : typing.Optional[str] + + session_id : typing.Optional[str] + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) + + order_by : typing.Optional[str] + Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc + + tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Only traces that include all of these tags will be returned. + + version : typing.Optional[str] + Optional filter to only include traces with a certain version. + + release : typing.Optional[str] + Optional filter to only include traces with a certain release. + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for traces where the environment is one of the provided values. + + fields : typing.Optional[str] + Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, sessionId, tags, version, release, environment, fromTimestamp, toTimestamp). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Trace Fields + - `id` (string) - Trace ID + - `name` (string) - Trace name + - `timestamp` (datetime) - Trace timestamp + - `userId` (string) - User ID + - `sessionId` (string) - Session ID + - `environment` (string) - Environment tag + - `version` (string) - Version tag + - `release` (string) - Release tag + - `tags` (arrayOptions) - Array of tags + - `bookmarked` (boolean) - Bookmark status + + ### Structured Data + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + ### Aggregated Metrics (from observations) + These metrics are aggregated from all observations within the trace: + - `latency` (number) - Latency in seconds (time from first observation start to last observation end) + - `inputTokens` (number) - Total input tokens across all observations + - `outputTokens` (number) - Total output tokens across all observations + - `totalTokens` (number) - Total tokens (alias: `tokens`) + - `inputCost` (number) - Total input cost in USD + - `outputCost` (number) - Total output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Observation Level Aggregations + These fields aggregate observation levels within the trace: + - `level` (string) - Highest severity level (ERROR > WARNING > DEFAULT > DEBUG) + - `warningCount` (number) - Count of WARNING level observations + - `errorCount` (number) - Count of ERROR level observations + - `defaultCount` (number) - Count of DEFAULT level observations + - `debugCount` (number) - Count of DEBUG level observations + + ### Scores (requires join with scores table) + - `scores_avg` (number) - Average of numeric scores (alias: `scores`) + - `score_categories` (categoryOptions) - Categorical score values + + ## Filter Examples + ```json + [ + { + "type": "datetime", + "column": "timestamp", + "operator": ">=", + "value": "2024-01-01T00:00:00Z" + }, + { + "type": "string", + "column": "userId", + "operator": "=", + "value": "user-123" + }, + { + "type": "number", + "column": "totalCost", + "operator": ">=", + "value": 0.01 + }, + { + "type": "arrayOptions", + "column": "tags", + "operator": "all of", + "value": ["production", "critical"] + }, + { + "type": "stringObject", + "column": "metadata", + "key": "customer_tier", + "operator": "=", + "value": "enterprise" + } + ] + ``` + + ## Performance Notes + - Filtering on `userId`, `sessionId`, or `metadata` may enable skip indexes for better query performance + - Score filters require a join with the scores table and may impact query performance + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Traces + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.trace.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + page=page, + limit=limit, + user_id=user_id, + name=name, + session_id=session_id, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + order_by=order_by, + tags=tags, + version=version, + release=release, + environment=environment, + fields=fields, + filter=filter, + request_options=request_options, + ) + return _response.data + + async def delete_multiple( + self, + *, + trace_ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteTraceResponse: + """ + Delete multiple traces + + Parameters + ---------- + trace_ids : typing.Sequence[str] + List of trace IDs to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteTraceResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.trace.delete_multiple( + trace_ids=["traceIds", "traceIds"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_multiple( + trace_ids=trace_ids, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/trace/raw_client.py b/langfuse/api/trace/raw_client.py new file mode 100644 index 000000000..9e3d47c68 --- /dev/null +++ b/langfuse/api/trace/raw_client.py @@ -0,0 +1,1228 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ..commons.errors.access_denied_error import AccessDeniedError +from ..commons.errors.error import Error +from ..commons.errors.method_not_allowed_error import MethodNotAllowedError +from ..commons.errors.not_found_error import NotFoundError +from ..commons.errors.unauthorized_error import UnauthorizedError +from ..commons.types.trace_with_full_details import TraceWithFullDetails +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.datetime_utils import serialize_datetime +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import jsonable_encoder +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.delete_trace_response import DeleteTraceResponse +from .types.traces import Traces + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawTraceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get( + self, + trace_id: str, + *, + fields: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[TraceWithFullDetails]: + """ + Get a specific trace + + Parameters + ---------- + trace_id : str + The unique langfuse identifier of a trace + + fields : typing.Optional[str] + Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TraceWithFullDetails] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/traces/{jsonable_encoder(trace_id)}", + method="GET", + params={ + "fields": fields, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TraceWithFullDetails, + parse_obj_as( + type_=TraceWithFullDetails, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete( + self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[DeleteTraceResponse]: + """ + Delete a specific trace + + Parameters + ---------- + trace_id : str + The unique langfuse identifier of the trace to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteTraceResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/traces/{jsonable_encoder(trace_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteTraceResponse, + parse_obj_as( + type_=DeleteTraceResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + user_id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + order_by: typing.Optional[str] = None, + tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + version: typing.Optional[str] = None, + release: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + fields: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Traces]: + """ + Get list of traces + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1 + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + user_id : typing.Optional[str] + + name : typing.Optional[str] + + session_id : typing.Optional[str] + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) + + order_by : typing.Optional[str] + Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc + + tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Only traces that include all of these tags will be returned. + + version : typing.Optional[str] + Optional filter to only include traces with a certain version. + + release : typing.Optional[str] + Optional filter to only include traces with a certain release. + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for traces where the environment is one of the provided values. + + fields : typing.Optional[str] + Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, sessionId, tags, version, release, environment, fromTimestamp, toTimestamp). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Trace Fields + - `id` (string) - Trace ID + - `name` (string) - Trace name + - `timestamp` (datetime) - Trace timestamp + - `userId` (string) - User ID + - `sessionId` (string) - Session ID + - `environment` (string) - Environment tag + - `version` (string) - Version tag + - `release` (string) - Release tag + - `tags` (arrayOptions) - Array of tags + - `bookmarked` (boolean) - Bookmark status + + ### Structured Data + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + ### Aggregated Metrics (from observations) + These metrics are aggregated from all observations within the trace: + - `latency` (number) - Latency in seconds (time from first observation start to last observation end) + - `inputTokens` (number) - Total input tokens across all observations + - `outputTokens` (number) - Total output tokens across all observations + - `totalTokens` (number) - Total tokens (alias: `tokens`) + - `inputCost` (number) - Total input cost in USD + - `outputCost` (number) - Total output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Observation Level Aggregations + These fields aggregate observation levels within the trace: + - `level` (string) - Highest severity level (ERROR > WARNING > DEFAULT > DEBUG) + - `warningCount` (number) - Count of WARNING level observations + - `errorCount` (number) - Count of ERROR level observations + - `defaultCount` (number) - Count of DEFAULT level observations + - `debugCount` (number) - Count of DEBUG level observations + + ### Scores (requires join with scores table) + - `scores_avg` (number) - Average of numeric scores (alias: `scores`) + - `score_categories` (categoryOptions) - Categorical score values + + ## Filter Examples + ```json + [ + { + "type": "datetime", + "column": "timestamp", + "operator": ">=", + "value": "2024-01-01T00:00:00Z" + }, + { + "type": "string", + "column": "userId", + "operator": "=", + "value": "user-123" + }, + { + "type": "number", + "column": "totalCost", + "operator": ">=", + "value": 0.01 + }, + { + "type": "arrayOptions", + "column": "tags", + "operator": "all of", + "value": ["production", "critical"] + }, + { + "type": "stringObject", + "column": "metadata", + "key": "customer_tier", + "operator": "=", + "value": "enterprise" + } + ] + ``` + + ## Performance Notes + - Filtering on `userId`, `sessionId`, or `metadata` may enable skip indexes for better query performance + - Score filters require a join with the scores table and may impact query performance + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Traces] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/traces", + method="GET", + params={ + "page": page, + "limit": limit, + "userId": user_id, + "name": name, + "sessionId": session_id, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + "orderBy": order_by, + "tags": tags, + "version": version, + "release": release, + "environment": environment, + "fields": fields, + "filter": filter, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Traces, + parse_obj_as( + type_=Traces, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_multiple( + self, + *, + trace_ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DeleteTraceResponse]: + """ + Delete multiple traces + + Parameters + ---------- + trace_ids : typing.Sequence[str] + List of trace IDs to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteTraceResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/traces", + method="DELETE", + json={ + "traceIds": trace_ids, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteTraceResponse, + parse_obj_as( + type_=DeleteTraceResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawTraceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get( + self, + trace_id: str, + *, + fields: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[TraceWithFullDetails]: + """ + Get a specific trace + + Parameters + ---------- + trace_id : str + The unique langfuse identifier of a trace + + fields : typing.Optional[str] + Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TraceWithFullDetails] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/traces/{jsonable_encoder(trace_id)}", + method="GET", + params={ + "fields": fields, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TraceWithFullDetails, + parse_obj_as( + type_=TraceWithFullDetails, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete( + self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[DeleteTraceResponse]: + """ + Delete a specific trace + + Parameters + ---------- + trace_id : str + The unique langfuse identifier of the trace to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteTraceResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/traces/{jsonable_encoder(trace_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteTraceResponse, + parse_obj_as( + type_=DeleteTraceResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + user_id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + order_by: typing.Optional[str] = None, + tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + version: typing.Optional[str] = None, + release: typing.Optional[str] = None, + environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + fields: typing.Optional[str] = None, + filter: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Traces]: + """ + Get list of traces + + Parameters + ---------- + page : typing.Optional[int] + Page number, starts at 1 + + limit : typing.Optional[int] + Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. + + user_id : typing.Optional[str] + + name : typing.Optional[str] + + session_id : typing.Optional[str] + + from_timestamp : typing.Optional[dt.datetime] + Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) + + to_timestamp : typing.Optional[dt.datetime] + Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) + + order_by : typing.Optional[str] + Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc + + tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Only traces that include all of these tags will be returned. + + version : typing.Optional[str] + Optional filter to only include traces with a certain version. + + release : typing.Optional[str] + Optional filter to only include traces with a certain release. + + environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] + Optional filter for traces where the environment is one of the provided values. + + fields : typing.Optional[str] + Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. + + filter : typing.Optional[str] + JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, sessionId, tags, version, release, environment, fromTimestamp, toTimestamp). + + ## Filter Structure + Each filter condition has the following structure: + ```json + [ + { + "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" + "column": string, // Required. Column to filter on (see available columns below) + "operator": string, // Required. Operator based on type: + // - datetime: ">", "<", ">=", "<=" + // - string: "=", "contains", "does not contain", "starts with", "ends with" + // - stringOptions: "any of", "none of" + // - categoryOptions: "any of", "none of" + // - arrayOptions: "any of", "none of", "all of" + // - number: "=", ">", "<", ">=", "<=" + // - stringObject: "=", "contains", "does not contain", "starts with", "ends with" + // - numberObject: "=", ">", "<", ">=", "<=" + // - boolean: "=", "<>" + // - null: "is null", "is not null" + "value": any, // Required (except for null type). Value to compare against. Type depends on filter type + "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata + } + ] + ``` + + ## Available Columns + + ### Core Trace Fields + - `id` (string) - Trace ID + - `name` (string) - Trace name + - `timestamp` (datetime) - Trace timestamp + - `userId` (string) - User ID + - `sessionId` (string) - Session ID + - `environment` (string) - Environment tag + - `version` (string) - Version tag + - `release` (string) - Release tag + - `tags` (arrayOptions) - Array of tags + - `bookmarked` (boolean) - Bookmark status + + ### Structured Data + - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. + + ### Aggregated Metrics (from observations) + These metrics are aggregated from all observations within the trace: + - `latency` (number) - Latency in seconds (time from first observation start to last observation end) + - `inputTokens` (number) - Total input tokens across all observations + - `outputTokens` (number) - Total output tokens across all observations + - `totalTokens` (number) - Total tokens (alias: `tokens`) + - `inputCost` (number) - Total input cost in USD + - `outputCost` (number) - Total output cost in USD + - `totalCost` (number) - Total cost in USD + + ### Observation Level Aggregations + These fields aggregate observation levels within the trace: + - `level` (string) - Highest severity level (ERROR > WARNING > DEFAULT > DEBUG) + - `warningCount` (number) - Count of WARNING level observations + - `errorCount` (number) - Count of ERROR level observations + - `defaultCount` (number) - Count of DEFAULT level observations + - `debugCount` (number) - Count of DEBUG level observations + + ### Scores (requires join with scores table) + - `scores_avg` (number) - Average of numeric scores (alias: `scores`) + - `score_categories` (categoryOptions) - Categorical score values + + ## Filter Examples + ```json + [ + { + "type": "datetime", + "column": "timestamp", + "operator": ">=", + "value": "2024-01-01T00:00:00Z" + }, + { + "type": "string", + "column": "userId", + "operator": "=", + "value": "user-123" + }, + { + "type": "number", + "column": "totalCost", + "operator": ">=", + "value": 0.01 + }, + { + "type": "arrayOptions", + "column": "tags", + "operator": "all of", + "value": ["production", "critical"] + }, + { + "type": "stringObject", + "column": "metadata", + "key": "customer_tier", + "operator": "=", + "value": "enterprise" + } + ] + ``` + + ## Performance Notes + - Filtering on `userId`, `sessionId`, or `metadata` may enable skip indexes for better query performance + - Score filters require a join with the scores table and may impact query performance + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Traces] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/traces", + method="GET", + params={ + "page": page, + "limit": limit, + "userId": user_id, + "name": name, + "sessionId": session_id, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + "orderBy": order_by, + "tags": tags, + "version": version, + "release": release, + "environment": environment, + "fields": fields, + "filter": filter, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Traces, + parse_obj_as( + type_=Traces, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_multiple( + self, + *, + trace_ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DeleteTraceResponse]: + """ + Delete multiple traces + + Parameters + ---------- + trace_ids : typing.Sequence[str] + List of trace IDs to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteTraceResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/traces", + method="DELETE", + json={ + "traceIds": trace_ids, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteTraceResponse, + parse_obj_as( + type_=DeleteTraceResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/trace/types/__init__.py b/langfuse/api/trace/types/__init__.py new file mode 100644 index 000000000..3eab30d21 --- /dev/null +++ b/langfuse/api/trace/types/__init__.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .delete_trace_response import DeleteTraceResponse + from .sort import Sort + from .traces import Traces +_dynamic_imports: typing.Dict[str, str] = { + "DeleteTraceResponse": ".delete_trace_response", + "Sort": ".sort", + "Traces": ".traces", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["DeleteTraceResponse", "Sort", "Traces"] diff --git a/langfuse/api/trace/types/delete_trace_response.py b/langfuse/api/trace/types/delete_trace_response.py new file mode 100644 index 000000000..173075a83 --- /dev/null +++ b/langfuse/api/trace/types/delete_trace_response.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class DeleteTraceResponse(UniversalBaseModel): + message: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/trace/types/sort.py b/langfuse/api/trace/types/sort.py new file mode 100644 index 000000000..b6b992870 --- /dev/null +++ b/langfuse/api/trace/types/sort.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import UniversalBaseModel + + +class Sort(UniversalBaseModel): + id: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/trace/types/traces.py b/langfuse/api/trace/types/traces.py new file mode 100644 index 000000000..b559118e1 --- /dev/null +++ b/langfuse/api/trace/types/traces.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...commons.types.trace_with_details import TraceWithDetails +from ...core.pydantic_utilities import UniversalBaseModel +from ...utils.pagination.types.meta_response import MetaResponse + + +class Traces(UniversalBaseModel): + data: typing.List[TraceWithDetails] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/__init__.py b/langfuse/api/unstable/__init__.py new file mode 100644 index 000000000..e5356c235 --- /dev/null +++ b/langfuse/api/unstable/__init__.py @@ -0,0 +1,316 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .errors import ( + AccessDeniedError, + BadRequestError, + ConflictError, + InternalServerError, + MethodNotAllowedError, + NotFoundError, + PublicApiError, + PublicApiErrorCode, + PublicApiErrorDetails, + PublicApiValidationIssue, + TooManyRequestsError, + UnauthorizedError, + UnprocessableContentError, + ) + from . import commons, errors, evaluation_rules, evaluators + from .commons import ( + ArrayOptionsEvaluationRuleFilter, + BooleanEvaluationRuleFilter, + CategoryOptionsEvaluationRuleFilter, + CodeEvaluatorSourceCodeLanguage, + DateTimeEvaluationRuleFilter, + EvaluationRuleArrayOptionsFilterOperator, + EvaluationRuleBooleanFilterOperator, + EvaluationRuleFilter, + EvaluationRuleFilter_ArrayOptions, + EvaluationRuleFilter_Boolean, + EvaluationRuleFilter_CategoryOptions, + EvaluationRuleFilter_Datetime, + EvaluationRuleFilter_Null, + EvaluationRuleFilter_Number, + EvaluationRuleFilter_NumberObject, + EvaluationRuleFilter_String, + EvaluationRuleFilter_StringObject, + EvaluationRuleFilter_StringOptions, + EvaluationRuleMapping, + EvaluationRuleMappingSource, + EvaluationRuleNullFilterOperator, + EvaluationRuleNumberFilterOperator, + EvaluationRuleOptionsFilterOperator, + EvaluationRuleStatus, + EvaluationRuleStringFilterOperator, + EvaluationRuleTarget, + EvaluatorModelConfig, + EvaluatorOutputDataType, + EvaluatorOutputDefinition, + EvaluatorOutputDefinition_Boolean, + EvaluatorOutputDefinition_Categorical, + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputFieldDefinition, + EvaluatorScope, + EvaluatorType, + NullEvaluationRuleFilter, + NumberEvaluationRuleFilter, + NumberObjectEvaluationRuleFilter, + PublicBooleanEvaluatorOutputDefinition, + PublicCategoricalEvaluatorOutputDefinition, + PublicCategoricalEvaluatorOutputScoreDefinition, + PublicEvaluatorOutputDefinition, + PublicEvaluatorOutputDefinition_Boolean, + PublicEvaluatorOutputDefinition_Categorical, + PublicEvaluatorOutputDefinition_Numeric, + PublicNumericEvaluatorOutputDefinition, + StringEvaluationRuleFilter, + StringObjectEvaluationRuleFilter, + StringOptionsEvaluationRuleFilter, + ) + from .evaluation_rules import ( + CodeEvaluationRuleEvaluatorReference, + CreateCodeEvaluationRuleRequest, + CreateEvaluationRuleRequest, + CreateLlmAsJudgeEvaluationRuleRequest, + DeleteEvaluationRuleResponse, + EvaluationRule, + EvaluationRuleEvaluator, + EvaluationRuleEvaluatorReference, + EvaluationRules, + LlmAsJudgeEvaluationRuleEvaluatorReference, + LlmAsJudgeEvaluatorType, + UpdateEvaluationRuleRequest, + ) + from .evaluators import ( + CodeEvaluator, + CreateCodeEvaluatorRequest, + CreateEvaluatorRequest, + CreateEvaluatorRequest_Code, + CreateEvaluatorRequest_LlmAsJudge, + CreateLlmAsJudgeEvaluatorRequest, + Evaluator, + EvaluatorBase, + Evaluator_Code, + Evaluator_LlmAsJudge, + Evaluators, + LlmAsJudgeEvaluator, + ) +_dynamic_imports: typing.Dict[str, str] = { + "AccessDeniedError": ".errors", + "ArrayOptionsEvaluationRuleFilter": ".commons", + "BadRequestError": ".errors", + "BooleanEvaluationRuleFilter": ".commons", + "CategoryOptionsEvaluationRuleFilter": ".commons", + "CodeEvaluationRuleEvaluatorReference": ".evaluation_rules", + "CodeEvaluator": ".evaluators", + "CodeEvaluatorSourceCodeLanguage": ".commons", + "ConflictError": ".errors", + "CreateCodeEvaluationRuleRequest": ".evaluation_rules", + "CreateCodeEvaluatorRequest": ".evaluators", + "CreateEvaluationRuleRequest": ".evaluation_rules", + "CreateEvaluatorRequest": ".evaluators", + "CreateEvaluatorRequest_Code": ".evaluators", + "CreateEvaluatorRequest_LlmAsJudge": ".evaluators", + "CreateLlmAsJudgeEvaluationRuleRequest": ".evaluation_rules", + "CreateLlmAsJudgeEvaluatorRequest": ".evaluators", + "DateTimeEvaluationRuleFilter": ".commons", + "DeleteEvaluationRuleResponse": ".evaluation_rules", + "EvaluationRule": ".evaluation_rules", + "EvaluationRuleArrayOptionsFilterOperator": ".commons", + "EvaluationRuleBooleanFilterOperator": ".commons", + "EvaluationRuleEvaluator": ".evaluation_rules", + "EvaluationRuleEvaluatorReference": ".evaluation_rules", + "EvaluationRuleFilter": ".commons", + "EvaluationRuleFilter_ArrayOptions": ".commons", + "EvaluationRuleFilter_Boolean": ".commons", + "EvaluationRuleFilter_CategoryOptions": ".commons", + "EvaluationRuleFilter_Datetime": ".commons", + "EvaluationRuleFilter_Null": ".commons", + "EvaluationRuleFilter_Number": ".commons", + "EvaluationRuleFilter_NumberObject": ".commons", + "EvaluationRuleFilter_String": ".commons", + "EvaluationRuleFilter_StringObject": ".commons", + "EvaluationRuleFilter_StringOptions": ".commons", + "EvaluationRuleMapping": ".commons", + "EvaluationRuleMappingSource": ".commons", + "EvaluationRuleNullFilterOperator": ".commons", + "EvaluationRuleNumberFilterOperator": ".commons", + "EvaluationRuleOptionsFilterOperator": ".commons", + "EvaluationRuleStatus": ".commons", + "EvaluationRuleStringFilterOperator": ".commons", + "EvaluationRuleTarget": ".commons", + "EvaluationRules": ".evaluation_rules", + "Evaluator": ".evaluators", + "EvaluatorBase": ".evaluators", + "EvaluatorModelConfig": ".commons", + "EvaluatorOutputDataType": ".commons", + "EvaluatorOutputDefinition": ".commons", + "EvaluatorOutputDefinition_Boolean": ".commons", + "EvaluatorOutputDefinition_Categorical": ".commons", + "EvaluatorOutputDefinition_Numeric": ".commons", + "EvaluatorOutputFieldDefinition": ".commons", + "EvaluatorScope": ".commons", + "EvaluatorType": ".commons", + "Evaluator_Code": ".evaluators", + "Evaluator_LlmAsJudge": ".evaluators", + "Evaluators": ".evaluators", + "InternalServerError": ".errors", + "LlmAsJudgeEvaluationRuleEvaluatorReference": ".evaluation_rules", + "LlmAsJudgeEvaluator": ".evaluators", + "LlmAsJudgeEvaluatorType": ".evaluation_rules", + "MethodNotAllowedError": ".errors", + "NotFoundError": ".errors", + "NullEvaluationRuleFilter": ".commons", + "NumberEvaluationRuleFilter": ".commons", + "NumberObjectEvaluationRuleFilter": ".commons", + "PublicApiError": ".errors", + "PublicApiErrorCode": ".errors", + "PublicApiErrorDetails": ".errors", + "PublicApiValidationIssue": ".errors", + "PublicBooleanEvaluatorOutputDefinition": ".commons", + "PublicCategoricalEvaluatorOutputDefinition": ".commons", + "PublicCategoricalEvaluatorOutputScoreDefinition": ".commons", + "PublicEvaluatorOutputDefinition": ".commons", + "PublicEvaluatorOutputDefinition_Boolean": ".commons", + "PublicEvaluatorOutputDefinition_Categorical": ".commons", + "PublicEvaluatorOutputDefinition_Numeric": ".commons", + "PublicNumericEvaluatorOutputDefinition": ".commons", + "StringEvaluationRuleFilter": ".commons", + "StringObjectEvaluationRuleFilter": ".commons", + "StringOptionsEvaluationRuleFilter": ".commons", + "TooManyRequestsError": ".errors", + "UnauthorizedError": ".errors", + "UnprocessableContentError": ".errors", + "UpdateEvaluationRuleRequest": ".evaluation_rules", + "commons": ".commons", + "errors": ".errors", + "evaluation_rules": ".evaluation_rules", + "evaluators": ".evaluators", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AccessDeniedError", + "ArrayOptionsEvaluationRuleFilter", + "BadRequestError", + "BooleanEvaluationRuleFilter", + "CategoryOptionsEvaluationRuleFilter", + "CodeEvaluationRuleEvaluatorReference", + "CodeEvaluator", + "CodeEvaluatorSourceCodeLanguage", + "ConflictError", + "CreateCodeEvaluationRuleRequest", + "CreateCodeEvaluatorRequest", + "CreateEvaluationRuleRequest", + "CreateEvaluatorRequest", + "CreateEvaluatorRequest_Code", + "CreateEvaluatorRequest_LlmAsJudge", + "CreateLlmAsJudgeEvaluationRuleRequest", + "CreateLlmAsJudgeEvaluatorRequest", + "DateTimeEvaluationRuleFilter", + "DeleteEvaluationRuleResponse", + "EvaluationRule", + "EvaluationRuleArrayOptionsFilterOperator", + "EvaluationRuleBooleanFilterOperator", + "EvaluationRuleEvaluator", + "EvaluationRuleEvaluatorReference", + "EvaluationRuleFilter", + "EvaluationRuleFilter_ArrayOptions", + "EvaluationRuleFilter_Boolean", + "EvaluationRuleFilter_CategoryOptions", + "EvaluationRuleFilter_Datetime", + "EvaluationRuleFilter_Null", + "EvaluationRuleFilter_Number", + "EvaluationRuleFilter_NumberObject", + "EvaluationRuleFilter_String", + "EvaluationRuleFilter_StringObject", + "EvaluationRuleFilter_StringOptions", + "EvaluationRuleMapping", + "EvaluationRuleMappingSource", + "EvaluationRuleNullFilterOperator", + "EvaluationRuleNumberFilterOperator", + "EvaluationRuleOptionsFilterOperator", + "EvaluationRuleStatus", + "EvaluationRuleStringFilterOperator", + "EvaluationRuleTarget", + "EvaluationRules", + "Evaluator", + "EvaluatorBase", + "EvaluatorModelConfig", + "EvaluatorOutputDataType", + "EvaluatorOutputDefinition", + "EvaluatorOutputDefinition_Boolean", + "EvaluatorOutputDefinition_Categorical", + "EvaluatorOutputDefinition_Numeric", + "EvaluatorOutputFieldDefinition", + "EvaluatorScope", + "EvaluatorType", + "Evaluator_Code", + "Evaluator_LlmAsJudge", + "Evaluators", + "InternalServerError", + "LlmAsJudgeEvaluationRuleEvaluatorReference", + "LlmAsJudgeEvaluator", + "LlmAsJudgeEvaluatorType", + "MethodNotAllowedError", + "NotFoundError", + "NullEvaluationRuleFilter", + "NumberEvaluationRuleFilter", + "NumberObjectEvaluationRuleFilter", + "PublicApiError", + "PublicApiErrorCode", + "PublicApiErrorDetails", + "PublicApiValidationIssue", + "PublicBooleanEvaluatorOutputDefinition", + "PublicCategoricalEvaluatorOutputDefinition", + "PublicCategoricalEvaluatorOutputScoreDefinition", + "PublicEvaluatorOutputDefinition", + "PublicEvaluatorOutputDefinition_Boolean", + "PublicEvaluatorOutputDefinition_Categorical", + "PublicEvaluatorOutputDefinition_Numeric", + "PublicNumericEvaluatorOutputDefinition", + "StringEvaluationRuleFilter", + "StringObjectEvaluationRuleFilter", + "StringOptionsEvaluationRuleFilter", + "TooManyRequestsError", + "UnauthorizedError", + "UnprocessableContentError", + "UpdateEvaluationRuleRequest", + "commons", + "errors", + "evaluation_rules", + "evaluators", +] diff --git a/langfuse/api/unstable/client.py b/langfuse/api/unstable/client.py new file mode 100644 index 000000000..5c3ac32d7 --- /dev/null +++ b/langfuse/api/unstable/client.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .raw_client import AsyncRawUnstableClient, RawUnstableClient + +if typing.TYPE_CHECKING: + from .evaluation_rules.client import ( + AsyncEvaluationRulesClient, + EvaluationRulesClient, + ) + from .evaluators.client import AsyncEvaluatorsClient, EvaluatorsClient + + +class UnstableClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawUnstableClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._evaluation_rules: typing.Optional[EvaluationRulesClient] = None + self._evaluators: typing.Optional[EvaluatorsClient] = None + + @property + def with_raw_response(self) -> RawUnstableClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawUnstableClient + """ + return self._raw_client + + @property + def evaluation_rules(self): + if self._evaluation_rules is None: + from .evaluation_rules.client import EvaluationRulesClient # noqa: E402 + + self._evaluation_rules = EvaluationRulesClient( + client_wrapper=self._client_wrapper + ) + return self._evaluation_rules + + @property + def evaluators(self): + if self._evaluators is None: + from .evaluators.client import EvaluatorsClient # noqa: E402 + + self._evaluators = EvaluatorsClient(client_wrapper=self._client_wrapper) + return self._evaluators + + +class AsyncUnstableClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawUnstableClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._evaluation_rules: typing.Optional[AsyncEvaluationRulesClient] = None + self._evaluators: typing.Optional[AsyncEvaluatorsClient] = None + + @property + def with_raw_response(self) -> AsyncRawUnstableClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawUnstableClient + """ + return self._raw_client + + @property + def evaluation_rules(self): + if self._evaluation_rules is None: + from .evaluation_rules.client import AsyncEvaluationRulesClient # noqa: E402 + + self._evaluation_rules = AsyncEvaluationRulesClient( + client_wrapper=self._client_wrapper + ) + return self._evaluation_rules + + @property + def evaluators(self): + if self._evaluators is None: + from .evaluators.client import AsyncEvaluatorsClient # noqa: E402 + + self._evaluators = AsyncEvaluatorsClient( + client_wrapper=self._client_wrapper + ) + return self._evaluators diff --git a/langfuse/api/unstable/commons/__init__.py b/langfuse/api/unstable/commons/__init__.py new file mode 100644 index 000000000..c617b53c7 --- /dev/null +++ b/langfuse/api/unstable/commons/__init__.py @@ -0,0 +1,190 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + ArrayOptionsEvaluationRuleFilter, + BooleanEvaluationRuleFilter, + CategoryOptionsEvaluationRuleFilter, + CodeEvaluatorSourceCodeLanguage, + DateTimeEvaluationRuleFilter, + EvaluationRuleArrayOptionsFilterOperator, + EvaluationRuleBooleanFilterOperator, + EvaluationRuleFilter, + EvaluationRuleFilter_ArrayOptions, + EvaluationRuleFilter_Boolean, + EvaluationRuleFilter_CategoryOptions, + EvaluationRuleFilter_Datetime, + EvaluationRuleFilter_Null, + EvaluationRuleFilter_Number, + EvaluationRuleFilter_NumberObject, + EvaluationRuleFilter_String, + EvaluationRuleFilter_StringObject, + EvaluationRuleFilter_StringOptions, + EvaluationRuleMapping, + EvaluationRuleMappingSource, + EvaluationRuleNullFilterOperator, + EvaluationRuleNumberFilterOperator, + EvaluationRuleOptionsFilterOperator, + EvaluationRuleStatus, + EvaluationRuleStringFilterOperator, + EvaluationRuleTarget, + EvaluatorModelConfig, + EvaluatorOutputDataType, + EvaluatorOutputDefinition, + EvaluatorOutputDefinition_Boolean, + EvaluatorOutputDefinition_Categorical, + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputFieldDefinition, + EvaluatorScope, + EvaluatorType, + NullEvaluationRuleFilter, + NumberEvaluationRuleFilter, + NumberObjectEvaluationRuleFilter, + PublicBooleanEvaluatorOutputDefinition, + PublicCategoricalEvaluatorOutputDefinition, + PublicCategoricalEvaluatorOutputScoreDefinition, + PublicEvaluatorOutputDefinition, + PublicEvaluatorOutputDefinition_Boolean, + PublicEvaluatorOutputDefinition_Categorical, + PublicEvaluatorOutputDefinition_Numeric, + PublicNumericEvaluatorOutputDefinition, + StringEvaluationRuleFilter, + StringObjectEvaluationRuleFilter, + StringOptionsEvaluationRuleFilter, + ) +_dynamic_imports: typing.Dict[str, str] = { + "ArrayOptionsEvaluationRuleFilter": ".types", + "BooleanEvaluationRuleFilter": ".types", + "CategoryOptionsEvaluationRuleFilter": ".types", + "CodeEvaluatorSourceCodeLanguage": ".types", + "DateTimeEvaluationRuleFilter": ".types", + "EvaluationRuleArrayOptionsFilterOperator": ".types", + "EvaluationRuleBooleanFilterOperator": ".types", + "EvaluationRuleFilter": ".types", + "EvaluationRuleFilter_ArrayOptions": ".types", + "EvaluationRuleFilter_Boolean": ".types", + "EvaluationRuleFilter_CategoryOptions": ".types", + "EvaluationRuleFilter_Datetime": ".types", + "EvaluationRuleFilter_Null": ".types", + "EvaluationRuleFilter_Number": ".types", + "EvaluationRuleFilter_NumberObject": ".types", + "EvaluationRuleFilter_String": ".types", + "EvaluationRuleFilter_StringObject": ".types", + "EvaluationRuleFilter_StringOptions": ".types", + "EvaluationRuleMapping": ".types", + "EvaluationRuleMappingSource": ".types", + "EvaluationRuleNullFilterOperator": ".types", + "EvaluationRuleNumberFilterOperator": ".types", + "EvaluationRuleOptionsFilterOperator": ".types", + "EvaluationRuleStatus": ".types", + "EvaluationRuleStringFilterOperator": ".types", + "EvaluationRuleTarget": ".types", + "EvaluatorModelConfig": ".types", + "EvaluatorOutputDataType": ".types", + "EvaluatorOutputDefinition": ".types", + "EvaluatorOutputDefinition_Boolean": ".types", + "EvaluatorOutputDefinition_Categorical": ".types", + "EvaluatorOutputDefinition_Numeric": ".types", + "EvaluatorOutputFieldDefinition": ".types", + "EvaluatorScope": ".types", + "EvaluatorType": ".types", + "NullEvaluationRuleFilter": ".types", + "NumberEvaluationRuleFilter": ".types", + "NumberObjectEvaluationRuleFilter": ".types", + "PublicBooleanEvaluatorOutputDefinition": ".types", + "PublicCategoricalEvaluatorOutputDefinition": ".types", + "PublicCategoricalEvaluatorOutputScoreDefinition": ".types", + "PublicEvaluatorOutputDefinition": ".types", + "PublicEvaluatorOutputDefinition_Boolean": ".types", + "PublicEvaluatorOutputDefinition_Categorical": ".types", + "PublicEvaluatorOutputDefinition_Numeric": ".types", + "PublicNumericEvaluatorOutputDefinition": ".types", + "StringEvaluationRuleFilter": ".types", + "StringObjectEvaluationRuleFilter": ".types", + "StringOptionsEvaluationRuleFilter": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ArrayOptionsEvaluationRuleFilter", + "BooleanEvaluationRuleFilter", + "CategoryOptionsEvaluationRuleFilter", + "CodeEvaluatorSourceCodeLanguage", + "DateTimeEvaluationRuleFilter", + "EvaluationRuleArrayOptionsFilterOperator", + "EvaluationRuleBooleanFilterOperator", + "EvaluationRuleFilter", + "EvaluationRuleFilter_ArrayOptions", + "EvaluationRuleFilter_Boolean", + "EvaluationRuleFilter_CategoryOptions", + "EvaluationRuleFilter_Datetime", + "EvaluationRuleFilter_Null", + "EvaluationRuleFilter_Number", + "EvaluationRuleFilter_NumberObject", + "EvaluationRuleFilter_String", + "EvaluationRuleFilter_StringObject", + "EvaluationRuleFilter_StringOptions", + "EvaluationRuleMapping", + "EvaluationRuleMappingSource", + "EvaluationRuleNullFilterOperator", + "EvaluationRuleNumberFilterOperator", + "EvaluationRuleOptionsFilterOperator", + "EvaluationRuleStatus", + "EvaluationRuleStringFilterOperator", + "EvaluationRuleTarget", + "EvaluatorModelConfig", + "EvaluatorOutputDataType", + "EvaluatorOutputDefinition", + "EvaluatorOutputDefinition_Boolean", + "EvaluatorOutputDefinition_Categorical", + "EvaluatorOutputDefinition_Numeric", + "EvaluatorOutputFieldDefinition", + "EvaluatorScope", + "EvaluatorType", + "NullEvaluationRuleFilter", + "NumberEvaluationRuleFilter", + "NumberObjectEvaluationRuleFilter", + "PublicBooleanEvaluatorOutputDefinition", + "PublicCategoricalEvaluatorOutputDefinition", + "PublicCategoricalEvaluatorOutputScoreDefinition", + "PublicEvaluatorOutputDefinition", + "PublicEvaluatorOutputDefinition_Boolean", + "PublicEvaluatorOutputDefinition_Categorical", + "PublicEvaluatorOutputDefinition_Numeric", + "PublicNumericEvaluatorOutputDefinition", + "StringEvaluationRuleFilter", + "StringObjectEvaluationRuleFilter", + "StringOptionsEvaluationRuleFilter", +] diff --git a/langfuse/api/unstable/commons/types/__init__.py b/langfuse/api/unstable/commons/types/__init__.py new file mode 100644 index 000000000..487480da4 --- /dev/null +++ b/langfuse/api/unstable/commons/types/__init__.py @@ -0,0 +1,214 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .array_options_evaluation_rule_filter import ArrayOptionsEvaluationRuleFilter + from .boolean_evaluation_rule_filter import BooleanEvaluationRuleFilter + from .category_options_evaluation_rule_filter import ( + CategoryOptionsEvaluationRuleFilter, + ) + from .code_evaluator_source_code_language import CodeEvaluatorSourceCodeLanguage + from .date_time_evaluation_rule_filter import DateTimeEvaluationRuleFilter + from .evaluation_rule_array_options_filter_operator import ( + EvaluationRuleArrayOptionsFilterOperator, + ) + from .evaluation_rule_boolean_filter_operator import ( + EvaluationRuleBooleanFilterOperator, + ) + from .evaluation_rule_filter import ( + EvaluationRuleFilter, + EvaluationRuleFilter_ArrayOptions, + EvaluationRuleFilter_Boolean, + EvaluationRuleFilter_CategoryOptions, + EvaluationRuleFilter_Datetime, + EvaluationRuleFilter_Null, + EvaluationRuleFilter_Number, + EvaluationRuleFilter_NumberObject, + EvaluationRuleFilter_String, + EvaluationRuleFilter_StringObject, + EvaluationRuleFilter_StringOptions, + ) + from .evaluation_rule_mapping import EvaluationRuleMapping + from .evaluation_rule_mapping_source import EvaluationRuleMappingSource + from .evaluation_rule_null_filter_operator import EvaluationRuleNullFilterOperator + from .evaluation_rule_number_filter_operator import ( + EvaluationRuleNumberFilterOperator, + ) + from .evaluation_rule_options_filter_operator import ( + EvaluationRuleOptionsFilterOperator, + ) + from .evaluation_rule_status import EvaluationRuleStatus + from .evaluation_rule_string_filter_operator import ( + EvaluationRuleStringFilterOperator, + ) + from .evaluation_rule_target import EvaluationRuleTarget + from .evaluator_model_config import EvaluatorModelConfig + from .evaluator_output_data_type import EvaluatorOutputDataType + from .evaluator_output_definition import ( + EvaluatorOutputDefinition, + EvaluatorOutputDefinition_Boolean, + EvaluatorOutputDefinition_Categorical, + EvaluatorOutputDefinition_Numeric, + ) + from .evaluator_output_field_definition import EvaluatorOutputFieldDefinition + from .evaluator_scope import EvaluatorScope + from .evaluator_type import EvaluatorType + from .null_evaluation_rule_filter import NullEvaluationRuleFilter + from .number_evaluation_rule_filter import NumberEvaluationRuleFilter + from .number_object_evaluation_rule_filter import NumberObjectEvaluationRuleFilter + from .public_boolean_evaluator_output_definition import ( + PublicBooleanEvaluatorOutputDefinition, + ) + from .public_categorical_evaluator_output_definition import ( + PublicCategoricalEvaluatorOutputDefinition, + ) + from .public_categorical_evaluator_output_score_definition import ( + PublicCategoricalEvaluatorOutputScoreDefinition, + ) + from .public_evaluator_output_definition import ( + PublicEvaluatorOutputDefinition, + PublicEvaluatorOutputDefinition_Boolean, + PublicEvaluatorOutputDefinition_Categorical, + PublicEvaluatorOutputDefinition_Numeric, + ) + from .public_numeric_evaluator_output_definition import ( + PublicNumericEvaluatorOutputDefinition, + ) + from .string_evaluation_rule_filter import StringEvaluationRuleFilter + from .string_object_evaluation_rule_filter import StringObjectEvaluationRuleFilter + from .string_options_evaluation_rule_filter import StringOptionsEvaluationRuleFilter +_dynamic_imports: typing.Dict[str, str] = { + "ArrayOptionsEvaluationRuleFilter": ".array_options_evaluation_rule_filter", + "BooleanEvaluationRuleFilter": ".boolean_evaluation_rule_filter", + "CategoryOptionsEvaluationRuleFilter": ".category_options_evaluation_rule_filter", + "CodeEvaluatorSourceCodeLanguage": ".code_evaluator_source_code_language", + "DateTimeEvaluationRuleFilter": ".date_time_evaluation_rule_filter", + "EvaluationRuleArrayOptionsFilterOperator": ".evaluation_rule_array_options_filter_operator", + "EvaluationRuleBooleanFilterOperator": ".evaluation_rule_boolean_filter_operator", + "EvaluationRuleFilter": ".evaluation_rule_filter", + "EvaluationRuleFilter_ArrayOptions": ".evaluation_rule_filter", + "EvaluationRuleFilter_Boolean": ".evaluation_rule_filter", + "EvaluationRuleFilter_CategoryOptions": ".evaluation_rule_filter", + "EvaluationRuleFilter_Datetime": ".evaluation_rule_filter", + "EvaluationRuleFilter_Null": ".evaluation_rule_filter", + "EvaluationRuleFilter_Number": ".evaluation_rule_filter", + "EvaluationRuleFilter_NumberObject": ".evaluation_rule_filter", + "EvaluationRuleFilter_String": ".evaluation_rule_filter", + "EvaluationRuleFilter_StringObject": ".evaluation_rule_filter", + "EvaluationRuleFilter_StringOptions": ".evaluation_rule_filter", + "EvaluationRuleMapping": ".evaluation_rule_mapping", + "EvaluationRuleMappingSource": ".evaluation_rule_mapping_source", + "EvaluationRuleNullFilterOperator": ".evaluation_rule_null_filter_operator", + "EvaluationRuleNumberFilterOperator": ".evaluation_rule_number_filter_operator", + "EvaluationRuleOptionsFilterOperator": ".evaluation_rule_options_filter_operator", + "EvaluationRuleStatus": ".evaluation_rule_status", + "EvaluationRuleStringFilterOperator": ".evaluation_rule_string_filter_operator", + "EvaluationRuleTarget": ".evaluation_rule_target", + "EvaluatorModelConfig": ".evaluator_model_config", + "EvaluatorOutputDataType": ".evaluator_output_data_type", + "EvaluatorOutputDefinition": ".evaluator_output_definition", + "EvaluatorOutputDefinition_Boolean": ".evaluator_output_definition", + "EvaluatorOutputDefinition_Categorical": ".evaluator_output_definition", + "EvaluatorOutputDefinition_Numeric": ".evaluator_output_definition", + "EvaluatorOutputFieldDefinition": ".evaluator_output_field_definition", + "EvaluatorScope": ".evaluator_scope", + "EvaluatorType": ".evaluator_type", + "NullEvaluationRuleFilter": ".null_evaluation_rule_filter", + "NumberEvaluationRuleFilter": ".number_evaluation_rule_filter", + "NumberObjectEvaluationRuleFilter": ".number_object_evaluation_rule_filter", + "PublicBooleanEvaluatorOutputDefinition": ".public_boolean_evaluator_output_definition", + "PublicCategoricalEvaluatorOutputDefinition": ".public_categorical_evaluator_output_definition", + "PublicCategoricalEvaluatorOutputScoreDefinition": ".public_categorical_evaluator_output_score_definition", + "PublicEvaluatorOutputDefinition": ".public_evaluator_output_definition", + "PublicEvaluatorOutputDefinition_Boolean": ".public_evaluator_output_definition", + "PublicEvaluatorOutputDefinition_Categorical": ".public_evaluator_output_definition", + "PublicEvaluatorOutputDefinition_Numeric": ".public_evaluator_output_definition", + "PublicNumericEvaluatorOutputDefinition": ".public_numeric_evaluator_output_definition", + "StringEvaluationRuleFilter": ".string_evaluation_rule_filter", + "StringObjectEvaluationRuleFilter": ".string_object_evaluation_rule_filter", + "StringOptionsEvaluationRuleFilter": ".string_options_evaluation_rule_filter", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ArrayOptionsEvaluationRuleFilter", + "BooleanEvaluationRuleFilter", + "CategoryOptionsEvaluationRuleFilter", + "CodeEvaluatorSourceCodeLanguage", + "DateTimeEvaluationRuleFilter", + "EvaluationRuleArrayOptionsFilterOperator", + "EvaluationRuleBooleanFilterOperator", + "EvaluationRuleFilter", + "EvaluationRuleFilter_ArrayOptions", + "EvaluationRuleFilter_Boolean", + "EvaluationRuleFilter_CategoryOptions", + "EvaluationRuleFilter_Datetime", + "EvaluationRuleFilter_Null", + "EvaluationRuleFilter_Number", + "EvaluationRuleFilter_NumberObject", + "EvaluationRuleFilter_String", + "EvaluationRuleFilter_StringObject", + "EvaluationRuleFilter_StringOptions", + "EvaluationRuleMapping", + "EvaluationRuleMappingSource", + "EvaluationRuleNullFilterOperator", + "EvaluationRuleNumberFilterOperator", + "EvaluationRuleOptionsFilterOperator", + "EvaluationRuleStatus", + "EvaluationRuleStringFilterOperator", + "EvaluationRuleTarget", + "EvaluatorModelConfig", + "EvaluatorOutputDataType", + "EvaluatorOutputDefinition", + "EvaluatorOutputDefinition_Boolean", + "EvaluatorOutputDefinition_Categorical", + "EvaluatorOutputDefinition_Numeric", + "EvaluatorOutputFieldDefinition", + "EvaluatorScope", + "EvaluatorType", + "NullEvaluationRuleFilter", + "NumberEvaluationRuleFilter", + "NumberObjectEvaluationRuleFilter", + "PublicBooleanEvaluatorOutputDefinition", + "PublicCategoricalEvaluatorOutputDefinition", + "PublicCategoricalEvaluatorOutputScoreDefinition", + "PublicEvaluatorOutputDefinition", + "PublicEvaluatorOutputDefinition_Boolean", + "PublicEvaluatorOutputDefinition_Categorical", + "PublicEvaluatorOutputDefinition_Numeric", + "PublicNumericEvaluatorOutputDefinition", + "StringEvaluationRuleFilter", + "StringObjectEvaluationRuleFilter", + "StringOptionsEvaluationRuleFilter", +] diff --git a/langfuse/api/unstable/commons/types/array_options_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/array_options_evaluation_rule_filter.py new file mode 100644 index 000000000..c89ce8b16 --- /dev/null +++ b/langfuse/api/unstable/commons/types/array_options_evaluation_rule_filter.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_array_options_filter_operator import ( + EvaluationRuleArrayOptionsFilterOperator, +) + + +class ArrayOptionsEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Column to filter on. + """ + + operator: EvaluationRuleArrayOptionsFilterOperator + value: typing.List[str] = pydantic.Field() + """ + One or more array elements to match. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/boolean_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/boolean_evaluation_rule_filter.py new file mode 100644 index 000000000..666b691bb --- /dev/null +++ b/langfuse/api/unstable/commons/types/boolean_evaluation_rule_filter.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_boolean_filter_operator import EvaluationRuleBooleanFilterOperator + + +class BooleanEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Column to filter on. + """ + + operator: EvaluationRuleBooleanFilterOperator + value: bool + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/category_options_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/category_options_evaluation_rule_filter.py new file mode 100644 index 000000000..97f13ae62 --- /dev/null +++ b/langfuse/api/unstable/commons/types/category_options_evaluation_rule_filter.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_options_filter_operator import EvaluationRuleOptionsFilterOperator + + +class CategoryOptionsEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Object-valued column to filter on. + """ + + key: str = pydantic.Field() + """ + Key inside the object-valued column to filter on. + """ + + operator: EvaluationRuleOptionsFilterOperator + value: typing.List[str] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/code_evaluator_source_code_language.py b/langfuse/api/unstable/commons/types/code_evaluator_source_code_language.py new file mode 100644 index 000000000..7071a317c --- /dev/null +++ b/langfuse/api/unstable/commons/types/code_evaluator_source_code_language.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class CodeEvaluatorSourceCodeLanguage(enum.StrEnum): + """ + Code evaluator runtime language. + """ + + PYTHON = "PYTHON" + TYPESCRIPT = "TYPESCRIPT" + + def visit( + self, + python: typing.Callable[[], T_Result], + typescript: typing.Callable[[], T_Result], + ) -> T_Result: + if self is CodeEvaluatorSourceCodeLanguage.PYTHON: + return python() + if self is CodeEvaluatorSourceCodeLanguage.TYPESCRIPT: + return typescript() diff --git a/langfuse/api/unstable/commons/types/date_time_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/date_time_evaluation_rule_filter.py new file mode 100644 index 000000000..9ee23b1fe --- /dev/null +++ b/langfuse/api/unstable/commons/types/date_time_evaluation_rule_filter.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_number_filter_operator import EvaluationRuleNumberFilterOperator + + +class DateTimeEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Column to filter on. + """ + + operator: EvaluationRuleNumberFilterOperator = pydantic.Field() + """ + Comparison operator for datetime values. + """ + + value: dt.datetime = pydantic.Field() + """ + Datetime value to compare against. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_array_options_filter_operator.py b/langfuse/api/unstable/commons/types/evaluation_rule_array_options_filter_operator.py new file mode 100644 index 000000000..ba8f49a13 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_array_options_filter_operator.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleArrayOptionsFilterOperator(enum.StrEnum): + ANY_OF = "any of" + NONE_OF = "none of" + ALL_OF = "all of" + + def visit( + self, + any_of: typing.Callable[[], T_Result], + none_of: typing.Callable[[], T_Result], + all_of: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleArrayOptionsFilterOperator.ANY_OF: + return any_of() + if self is EvaluationRuleArrayOptionsFilterOperator.NONE_OF: + return none_of() + if self is EvaluationRuleArrayOptionsFilterOperator.ALL_OF: + return all_of() diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_boolean_filter_operator.py b/langfuse/api/unstable/commons/types/evaluation_rule_boolean_filter_operator.py new file mode 100644 index 000000000..737d6063a --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_boolean_filter_operator.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleBooleanFilterOperator(enum.StrEnum): + EQUALS = "=" + NOT_EQUALS = "<>" + + def visit( + self, + equals: typing.Callable[[], T_Result], + not_equals: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleBooleanFilterOperator.EQUALS: + return equals() + if self is EvaluationRuleBooleanFilterOperator.NOT_EQUALS: + return not_equals() diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/evaluation_rule_filter.py new file mode 100644 index 000000000..ea5e0420b --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_filter.py @@ -0,0 +1,740 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_array_options_filter_operator import ( + EvaluationRuleArrayOptionsFilterOperator, +) +from .evaluation_rule_boolean_filter_operator import EvaluationRuleBooleanFilterOperator +from .evaluation_rule_null_filter_operator import EvaluationRuleNullFilterOperator +from .evaluation_rule_number_filter_operator import EvaluationRuleNumberFilterOperator +from .evaluation_rule_options_filter_operator import EvaluationRuleOptionsFilterOperator +from .evaluation_rule_string_filter_operator import EvaluationRuleStringFilterOperator + + +class EvaluationRuleFilter_Datetime(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["datetime"] = "datetime" + column: str + operator: EvaluationRuleNumberFilterOperator + value: dt.datetime + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_String(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["string"] = "string" + column: str + operator: EvaluationRuleStringFilterOperator + value: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_Number(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["number"] = "number" + column: str + operator: EvaluationRuleNumberFilterOperator + value: float + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_StringOptions(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["stringOptions"] = "stringOptions" + column: str + operator: EvaluationRuleOptionsFilterOperator + value: typing.List[str] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_CategoryOptions(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["categoryOptions"] = "categoryOptions" + column: str + key: str + operator: EvaluationRuleOptionsFilterOperator + value: typing.List[str] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_ArrayOptions(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["arrayOptions"] = "arrayOptions" + column: str + operator: EvaluationRuleArrayOptionsFilterOperator + value: typing.List[str] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_StringObject(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["stringObject"] = "stringObject" + column: str + key: str + operator: EvaluationRuleStringFilterOperator + value: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_NumberObject(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["numberObject"] = "numberObject" + column: str + key: str + operator: EvaluationRuleNumberFilterOperator + value: float + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_Boolean(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["boolean"] = "boolean" + column: str + operator: EvaluationRuleBooleanFilterOperator + value: bool + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluationRuleFilter_Null(UniversalBaseModel): + """ + One filter condition used to decide whether a live-ingested target should be evaluated. + + An evaluation rule can include zero or more filter objects. All filters must be satisfied for the target to run. + + How to build a valid filter object: + - Pick the `target` first, because it changes the supported columns. + - Pick the filter `type`. That determines which fields are required. + - Use `key` only for object filters such as `metadata`. + - Use the correct `value` shape for the chosen filter `type`. + + Operator quick reference by filter `type`: + - `string`: `"="`, `contains`, `does not contain`, `starts with`, `ends with` + - `number`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `datetime`: `"="`, `">"`, `"<"`, `">="`, `"<="` + - `stringOptions`: `any of`, `none of` + - `arrayOptions`: `any of`, `none of`, `all of` + - `stringObject`: same operators as `string` + - `null`: `is null`, `is not null` + + Supported columns by target: + - `target=observation` + - `type`: `stringOptions`, operators `any of` / `none of`, values `GENERATION`, `SPAN`, `EVENT` + - `name`: `stringOptions`, operators `any of` / `none of` + - `environment`: `stringOptions`, operators `any of` / `none of` + - `level`: `stringOptions`, operators `any of` / `none of`, values `DEBUG`, `DEFAULT`, `WARNING`, `ERROR` + - `version`: `string` + - `traceName`: `stringOptions`, operators `any of` / `none of` + - `userId`: `string` + - `sessionId`: `string` + - `tags`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `metadata`: `stringObject` with `key` + - `parentObservationId`: `null`, operators `is null` / `is not null` + - `calledToolNames`: `arrayOptions`, operators `any of` / `none of` / `all of` + - `toolCalls`: `number` + - `target=experiment` + - `datasetId`: `stringOptions`, operators `any of` / `none of` + Use dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + Recovery guidance: + - `invalid_filter_value` with `details.column` but no `invalidValues`: the selected `column` is not supported for the chosen `target` + - `invalid_filter_value` with `details.invalidValues`: the selected values are not allowed for that column. Replace them with one of `details.allowedValues` when provided. + - `invalid_filter_value` for `column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, + ) + + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + """ + + type: typing.Literal["null"] = "null" + column: str + operator: EvaluationRuleNullFilterOperator + value: typing.Optional[str] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +""" +from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleOptionsFilterOperator, +) + +EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], +) +""" +EvaluationRuleFilter = typing_extensions.Annotated[ + typing.Union[ + EvaluationRuleFilter_Datetime, + EvaluationRuleFilter_String, + EvaluationRuleFilter_Number, + EvaluationRuleFilter_StringOptions, + EvaluationRuleFilter_CategoryOptions, + EvaluationRuleFilter_ArrayOptions, + EvaluationRuleFilter_StringObject, + EvaluationRuleFilter_NumberObject, + EvaluationRuleFilter_Boolean, + EvaluationRuleFilter_Null, + ], + pydantic.Field(discriminator="type"), +] diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_mapping.py b/langfuse/api/unstable/commons/types/evaluation_rule_mapping.py new file mode 100644 index 000000000..cd08e8b33 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_mapping.py @@ -0,0 +1,76 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .evaluation_rule_mapping_source import EvaluationRuleMappingSource + + +class EvaluationRuleMapping(UniversalBaseModel): + """ + Maps one evaluator variable to one source field from the target object. + + Manual mappings are used for `llm_as_judge` evaluators. `code` evaluators use a fixed runtime mapping managed by Langfuse. + + How to build a valid mapping list: + 1. Create the evaluator or fetch it with `GET /evaluators/{id}`. + 2. Read the evaluator `variables` array. + 3. Add exactly one mapping object for each variable in that array. + 4. Use the variable name exactly as returned, without braces such as `{{` or `}}`. + 5. Choose a `source` that is valid for the selected `target`. + + `jsonPath` is optional. Use it only when the selected source is a JSON object and you want to extract one nested field before inserting it into the evaluator prompt. + + Recovery guidance: + - `invalid_variable_mapping`: the variable name is unknown for this evaluator, or the selected `source` is not valid for the chosen `target` + - `missing_variable_mapping`: one or more LLM-as-judge evaluator variables are not mapped yet + - `duplicate_variable_mapping`: the same evaluator variable appears more than once + - `invalid_json_path`: the JSONPath expression is malformed. Remove it or correct it. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluationRuleMapping, + EvaluationRuleMappingSource, + ) + + EvaluationRuleMapping( + variable="input", + source=EvaluationRuleMappingSource.INPUT, + ) + """ + + variable: str = pydantic.Field() + """ + Prompt variable name without braces. + + Example: for the prompt `Judge {{input}} against {{output}}`, use `input` and `output`. + """ + + source: EvaluationRuleMappingSource = pydantic.Field() + """ + Source field that should populate the prompt variable. + + Quick reference: + - `target=observation`: `input`, `output`, `metadata` + - `target=experiment`: `input`, `output`, `metadata`, `expected_output`, `experiment_item_metadata` + """ + + json_path: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="jsonPath") + ] = pydantic.Field(default=None) + """ + Optional JSONPath selector applied to the selected source before it is passed to the evaluator prompt. + + Requirements: + - Must start with `$` + - Must be a syntactically valid JSONPath expression + - Most useful with `source=metadata` + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_mapping_source.py b/langfuse/api/unstable/commons/types/evaluation_rule_mapping_source.py new file mode 100644 index 000000000..391c66bbd --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_mapping_source.py @@ -0,0 +1,51 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleMappingSource(enum.StrEnum): + """ + Source field used to populate a prompt variable. + + Use these values when mapping evaluator prompt variables to live data. + + Target-specific rules: + - `target=observation` supports `input`, `output`, and `metadata` + - `target=experiment` supports `input`, `output`, `metadata`, `expected_output`, and `experiment_item_metadata` + + Source semantics: + - `input`: the observation or experiment input payload + - `output`: the observation or experiment output payload + - `metadata`: the metadata object for the target. Combine with `jsonPath` when you need one nested field instead of the whole object. + - `expected_output`: the experiment item's expected output. Only valid for `target=experiment`. + - `experiment_item_metadata`: the experiment item's metadata object. Only valid for `target=experiment`. + """ + + INPUT = "input" + OUTPUT = "output" + METADATA = "metadata" + EXPECTED_OUTPUT = "expected_output" + EXPERIMENT_ITEM_METADATA = "experiment_item_metadata" + + def visit( + self, + input: typing.Callable[[], T_Result], + output: typing.Callable[[], T_Result], + metadata: typing.Callable[[], T_Result], + expected_output: typing.Callable[[], T_Result], + experiment_item_metadata: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleMappingSource.INPUT: + return input() + if self is EvaluationRuleMappingSource.OUTPUT: + return output() + if self is EvaluationRuleMappingSource.METADATA: + return metadata() + if self is EvaluationRuleMappingSource.EXPECTED_OUTPUT: + return expected_output() + if self is EvaluationRuleMappingSource.EXPERIMENT_ITEM_METADATA: + return experiment_item_metadata() diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_null_filter_operator.py b/langfuse/api/unstable/commons/types/evaluation_rule_null_filter_operator.py new file mode 100644 index 000000000..833c8406f --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_null_filter_operator.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleNullFilterOperator(enum.StrEnum): + IS_NULL = "is null" + IS_NOT_NULL = "is not null" + + def visit( + self, + is_null: typing.Callable[[], T_Result], + is_not_null: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleNullFilterOperator.IS_NULL: + return is_null() + if self is EvaluationRuleNullFilterOperator.IS_NOT_NULL: + return is_not_null() diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_number_filter_operator.py b/langfuse/api/unstable/commons/types/evaluation_rule_number_filter_operator.py new file mode 100644 index 000000000..927523e04 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_number_filter_operator.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleNumberFilterOperator(enum.StrEnum): + EQUALS = "=" + GREATER_THAN = ">" + LESS_THAN = "<" + GREATER_THAN_OR_EQUAL = ">=" + LESS_THAN_OR_EQUAL = "<=" + + def visit( + self, + equals: typing.Callable[[], T_Result], + greater_than: typing.Callable[[], T_Result], + less_than: typing.Callable[[], T_Result], + greater_than_or_equal: typing.Callable[[], T_Result], + less_than_or_equal: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleNumberFilterOperator.EQUALS: + return equals() + if self is EvaluationRuleNumberFilterOperator.GREATER_THAN: + return greater_than() + if self is EvaluationRuleNumberFilterOperator.LESS_THAN: + return less_than() + if self is EvaluationRuleNumberFilterOperator.GREATER_THAN_OR_EQUAL: + return greater_than_or_equal() + if self is EvaluationRuleNumberFilterOperator.LESS_THAN_OR_EQUAL: + return less_than_or_equal() diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_options_filter_operator.py b/langfuse/api/unstable/commons/types/evaluation_rule_options_filter_operator.py new file mode 100644 index 000000000..01cd13ea3 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_options_filter_operator.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleOptionsFilterOperator(enum.StrEnum): + ANY_OF = "any of" + NONE_OF = "none of" + + def visit( + self, + any_of: typing.Callable[[], T_Result], + none_of: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleOptionsFilterOperator.ANY_OF: + return any_of() + if self is EvaluationRuleOptionsFilterOperator.NONE_OF: + return none_of() diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_status.py b/langfuse/api/unstable/commons/types/evaluation_rule_status.py new file mode 100644 index 000000000..4a313a962 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_status.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleStatus(enum.StrEnum): + """ + Effective runtime status of the evaluation rule. + + - `active`: enabled and currently runnable. + - `inactive`: disabled by configuration. + - `paused`: enabled, but Langfuse has blocked execution until the underlying issue is resolved. + """ + + ACTIVE = "active" + INACTIVE = "inactive" + PAUSED = "paused" + + def visit( + self, + active: typing.Callable[[], T_Result], + inactive: typing.Callable[[], T_Result], + paused: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleStatus.ACTIVE: + return active() + if self is EvaluationRuleStatus.INACTIVE: + return inactive() + if self is EvaluationRuleStatus.PAUSED: + return paused() diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_string_filter_operator.py b/langfuse/api/unstable/commons/types/evaluation_rule_string_filter_operator.py new file mode 100644 index 000000000..9955172b9 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_string_filter_operator.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleStringFilterOperator(enum.StrEnum): + EQUALS = "=" + CONTAINS = "contains" + DOES_NOT_CONTAIN = "does not contain" + STARTS_WITH = "starts with" + ENDS_WITH = "ends with" + + def visit( + self, + equals: typing.Callable[[], T_Result], + contains: typing.Callable[[], T_Result], + does_not_contain: typing.Callable[[], T_Result], + starts_with: typing.Callable[[], T_Result], + ends_with: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleStringFilterOperator.EQUALS: + return equals() + if self is EvaluationRuleStringFilterOperator.CONTAINS: + return contains() + if self is EvaluationRuleStringFilterOperator.DOES_NOT_CONTAIN: + return does_not_contain() + if self is EvaluationRuleStringFilterOperator.STARTS_WITH: + return starts_with() + if self is EvaluationRuleStringFilterOperator.ENDS_WITH: + return ends_with() diff --git a/langfuse/api/unstable/commons/types/evaluation_rule_target.py b/langfuse/api/unstable/commons/types/evaluation_rule_target.py new file mode 100644 index 000000000..186aa461c --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluation_rule_target.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluationRuleTarget(enum.StrEnum): + """ + The ingestion object type that should trigger evaluation runs. + + Choose the target first, because it changes both the valid filter columns and the valid variable-mapping sources: + - `observation` evaluates live-ingested observations such as generations, spans, and events. + It supports mapping from `input`, `output`, and `metadata`. + - `experiment` evaluates live experiment executions and can additionally map `expected_output` and `experiment_item_metadata`. + It currently supports filtering by `datasetId`. + Discover valid dataset IDs with `GET /api/public/v2/datasets`, then use the returned dataset `id` values in your filter. + """ + + OBSERVATION = "observation" + EXPERIMENT = "experiment" + + def visit( + self, + observation: typing.Callable[[], T_Result], + experiment: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluationRuleTarget.OBSERVATION: + return observation() + if self is EvaluationRuleTarget.EXPERIMENT: + return experiment() diff --git a/langfuse/api/unstable/commons/types/evaluator_model_config.py b/langfuse/api/unstable/commons/types/evaluator_model_config.py new file mode 100644 index 000000000..5473cca8f --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluator_model_config.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel + + +class EvaluatorModelConfig(UniversalBaseModel): + """ + Optional explicit model configuration for an evaluator. + + If omitted, Langfuse uses the project's default evaluation model. + If provided, the model must be available to the project when the evaluator or evaluation rule is enabled. + + To discover valid configured `provider` values for a project, call `GET /api/public/llm-connections` and read the `provider` field from the returned connections. + Use a `provider` value that matches one of the connections already configured in the same project. + + Recovery guidance: + - If evaluator creation returns `422` with `code=evaluator_preflight_failed`, either provide a valid explicit `modelConfig` here or configure the project's default evaluation model, then retry the same request. + + Examples + -------- + from langfuse.unstable.commons import EvaluatorModelConfig + + EvaluatorModelConfig( + provider="openai", + model="gpt-4.1-mini", + ) + """ + + provider: str = pydantic.Field() + """ + Provider identifier to use for this evaluator, for example `openai` or `anthropic`. + + To discover valid values for the current project, call `GET /api/public/llm-connections` and use one of the returned `provider` values. + """ + + model: str = pydantic.Field() + """ + Model identifier exposed by the provider, for example `gpt-4.1-mini`. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/evaluator_output_data_type.py b/langfuse/api/unstable/commons/types/evaluator_output_data_type.py new file mode 100644 index 000000000..a6c309868 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluator_output_data_type.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluatorOutputDataType(enum.StrEnum): + """ + Structured score type returned by an evaluator. + + This controls the type of score value Langfuse stores for evaluation results: + - `NUMERIC`: a numeric score such as `0.82` + - `BOOLEAN`: a boolean score such as `true` + - `CATEGORICAL`: one or more category labels from a fixed list + """ + + NUMERIC = "NUMERIC" + BOOLEAN = "BOOLEAN" + CATEGORICAL = "CATEGORICAL" + + def visit( + self, + numeric: typing.Callable[[], T_Result], + boolean: typing.Callable[[], T_Result], + categorical: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluatorOutputDataType.NUMERIC: + return numeric() + if self is EvaluatorOutputDataType.BOOLEAN: + return boolean() + if self is EvaluatorOutputDataType.CATEGORICAL: + return categorical() diff --git a/langfuse/api/unstable/commons/types/evaluator_output_definition.py b/langfuse/api/unstable/commons/types/evaluator_output_definition.py new file mode 100644 index 000000000..f545a19a8 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluator_output_definition.py @@ -0,0 +1,161 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .evaluator_output_field_definition import EvaluatorOutputFieldDefinition +from .public_categorical_evaluator_output_score_definition import ( + PublicCategoricalEvaluatorOutputScoreDefinition, +) + + +class EvaluatorOutputDefinition_Numeric(UniversalBaseModel): + """ + Structured output definition to send when creating an evaluator. + + Agent guidance: + - `dataType` is required. + - Do not send `version`; that is an internal storage detail and is not part of the public request contract. + - For `NUMERIC` and `BOOLEAN`, provide `reasoning.description` and `score.description`. + - For `CATEGORICAL`, also provide `score.categories` and `score.shouldAllowMultipleMatches`. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluatorOutputDataType, + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputFieldDefinition, + ) + + EvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the answer is correct or incorrect.", + ), + score=EvaluatorOutputFieldDefinition( + description="Return a score between 0 and 1.", + ), + ) + """ + + data_type: typing_extensions.Annotated[ + typing.Literal["NUMERIC"], FieldMetadata(alias="dataType") + ] = "NUMERIC" + reasoning: EvaluatorOutputFieldDefinition + score: EvaluatorOutputFieldDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluatorOutputDefinition_Boolean(UniversalBaseModel): + """ + Structured output definition to send when creating an evaluator. + + Agent guidance: + - `dataType` is required. + - Do not send `version`; that is an internal storage detail and is not part of the public request contract. + - For `NUMERIC` and `BOOLEAN`, provide `reasoning.description` and `score.description`. + - For `CATEGORICAL`, also provide `score.categories` and `score.shouldAllowMultipleMatches`. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluatorOutputDataType, + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputFieldDefinition, + ) + + EvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the answer is correct or incorrect.", + ), + score=EvaluatorOutputFieldDefinition( + description="Return a score between 0 and 1.", + ), + ) + """ + + data_type: typing_extensions.Annotated[ + typing.Literal["BOOLEAN"], FieldMetadata(alias="dataType") + ] = "BOOLEAN" + reasoning: EvaluatorOutputFieldDefinition + score: EvaluatorOutputFieldDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class EvaluatorOutputDefinition_Categorical(UniversalBaseModel): + """ + Structured output definition to send when creating an evaluator. + + Agent guidance: + - `dataType` is required. + - Do not send `version`; that is an internal storage detail and is not part of the public request contract. + - For `NUMERIC` and `BOOLEAN`, provide `reasoning.description` and `score.description`. + - For `CATEGORICAL`, also provide `score.categories` and `score.shouldAllowMultipleMatches`. + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluatorOutputDataType, + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputFieldDefinition, + ) + + EvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the answer is correct or incorrect.", + ), + score=EvaluatorOutputFieldDefinition( + description="Return a score between 0 and 1.", + ), + ) + """ + + data_type: typing_extensions.Annotated[ + typing.Literal["CATEGORICAL"], FieldMetadata(alias="dataType") + ] = "CATEGORICAL" + reasoning: EvaluatorOutputFieldDefinition + score: PublicCategoricalEvaluatorOutputScoreDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +""" +from langfuse.unstable.commons import ( + EvaluatorOutputDataType, + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputFieldDefinition, +) + +EvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the answer is correct or incorrect.", + ), + score=EvaluatorOutputFieldDefinition( + description="Return a score between 0 and 1.", + ), +) +""" +EvaluatorOutputDefinition = typing_extensions.Annotated[ + typing.Union[ + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputDefinition_Boolean, + EvaluatorOutputDefinition_Categorical, + ], + pydantic.Field(discriminator="data_type"), +] diff --git a/langfuse/api/unstable/commons/types/evaluator_output_field_definition.py b/langfuse/api/unstable/commons/types/evaluator_output_field_definition.py new file mode 100644 index 000000000..419610d0a --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluator_output_field_definition.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel + + +class EvaluatorOutputFieldDefinition(UniversalBaseModel): + description: str = pydantic.Field() + """ + Human-readable instructions for what the evaluator should return in this field. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/evaluator_scope.py b/langfuse/api/unstable/commons/types/evaluator_scope.py new file mode 100644 index 000000000..7ce796418 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluator_scope.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluatorScope(enum.StrEnum): + """ + Where an evaluator comes from. + + - `project`: created in your project + - `managed`: provided by Langfuse + """ + + PROJECT = "project" + MANAGED = "managed" + + def visit( + self, + project: typing.Callable[[], T_Result], + managed: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluatorScope.PROJECT: + return project() + if self is EvaluatorScope.MANAGED: + return managed() diff --git a/langfuse/api/unstable/commons/types/evaluator_type.py b/langfuse/api/unstable/commons/types/evaluator_type.py new file mode 100644 index 000000000..f219fb7e1 --- /dev/null +++ b/langfuse/api/unstable/commons/types/evaluator_type.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class EvaluatorType(enum.StrEnum): + """ + The evaluator engine type. + + The unstable public API supports LLM-as-a-judge and code evaluators. + """ + + LLM_AS_JUDGE = "llm_as_judge" + CODE = "code" + + def visit( + self, + llm_as_judge: typing.Callable[[], T_Result], + code: typing.Callable[[], T_Result], + ) -> T_Result: + if self is EvaluatorType.LLM_AS_JUDGE: + return llm_as_judge() + if self is EvaluatorType.CODE: + return code() diff --git a/langfuse/api/unstable/commons/types/null_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/null_evaluation_rule_filter.py new file mode 100644 index 000000000..d224d7590 --- /dev/null +++ b/langfuse/api/unstable/commons/types/null_evaluation_rule_filter.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_null_filter_operator import EvaluationRuleNullFilterOperator + + +class NullEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Column to filter on. In the unstable public API this is currently `parentObservationId`. + """ + + operator: EvaluationRuleNullFilterOperator + value: typing.Optional[str] = pydantic.Field(default=None) + """ + Ignored placeholder value. Clients may omit it or send an empty string. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/number_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/number_evaluation_rule_filter.py new file mode 100644 index 000000000..f9c489291 --- /dev/null +++ b/langfuse/api/unstable/commons/types/number_evaluation_rule_filter.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_number_filter_operator import EvaluationRuleNumberFilterOperator + + +class NumberEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Column to filter on. + """ + + operator: EvaluationRuleNumberFilterOperator + value: float + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/number_object_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/number_object_evaluation_rule_filter.py new file mode 100644 index 000000000..fd9462174 --- /dev/null +++ b/langfuse/api/unstable/commons/types/number_object_evaluation_rule_filter.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_number_filter_operator import EvaluationRuleNumberFilterOperator + + +class NumberObjectEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Object-valued column to filter on. + """ + + key: str = pydantic.Field() + """ + Key inside the object-valued column to filter on. + """ + + operator: EvaluationRuleNumberFilterOperator + value: float + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/public_boolean_evaluator_output_definition.py b/langfuse/api/unstable/commons/types/public_boolean_evaluator_output_definition.py new file mode 100644 index 000000000..7baaf209a --- /dev/null +++ b/langfuse/api/unstable/commons/types/public_boolean_evaluator_output_definition.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .evaluator_output_data_type import EvaluatorOutputDataType +from .evaluator_output_field_definition import EvaluatorOutputFieldDefinition + + +class PublicBooleanEvaluatorOutputDefinition(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + EvaluatorOutputDataType, FieldMetadata(alias="dataType") + ] = pydantic.Field() + """ + Always `BOOLEAN`. + """ + + reasoning: EvaluatorOutputFieldDefinition + score: EvaluatorOutputFieldDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/public_categorical_evaluator_output_definition.py b/langfuse/api/unstable/commons/types/public_categorical_evaluator_output_definition.py new file mode 100644 index 000000000..30d4673bb --- /dev/null +++ b/langfuse/api/unstable/commons/types/public_categorical_evaluator_output_definition.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .evaluator_output_data_type import EvaluatorOutputDataType +from .evaluator_output_field_definition import EvaluatorOutputFieldDefinition +from .public_categorical_evaluator_output_score_definition import ( + PublicCategoricalEvaluatorOutputScoreDefinition, +) + + +class PublicCategoricalEvaluatorOutputDefinition(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + EvaluatorOutputDataType, FieldMetadata(alias="dataType") + ] = pydantic.Field() + """ + Always `CATEGORICAL`. + """ + + reasoning: EvaluatorOutputFieldDefinition + score: PublicCategoricalEvaluatorOutputScoreDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/public_categorical_evaluator_output_score_definition.py b/langfuse/api/unstable/commons/types/public_categorical_evaluator_output_score_definition.py new file mode 100644 index 000000000..81deadb93 --- /dev/null +++ b/langfuse/api/unstable/commons/types/public_categorical_evaluator_output_score_definition.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata + + +class PublicCategoricalEvaluatorOutputScoreDefinition(UniversalBaseModel): + description: str + categories: typing.List[str] + should_allow_multiple_matches: typing_extensions.Annotated[ + bool, FieldMetadata(alias="shouldAllowMultipleMatches") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/public_evaluator_output_definition.py b/langfuse/api/unstable/commons/types/public_evaluator_output_definition.py new file mode 100644 index 000000000..43c7aa9ba --- /dev/null +++ b/langfuse/api/unstable/commons/types/public_evaluator_output_definition.py @@ -0,0 +1,167 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .evaluator_output_field_definition import EvaluatorOutputFieldDefinition +from .public_categorical_evaluator_output_score_definition import ( + PublicCategoricalEvaluatorOutputScoreDefinition, +) + + +class PublicEvaluatorOutputDefinition_Numeric(UniversalBaseModel): + """ + Evaluator output definition returned by the public API. + + This response always includes `dataType` and never includes an internal output-definition `version`. + Legacy stored evaluator definitions are normalized into this shape before they are returned. + + Use this response shape when deciding how to interpret future evaluation scores: + - `NUMERIC`: expect numeric score values + - `BOOLEAN`: expect `true` / `false` + - `CATEGORICAL`: expect one or more values from `score.categories` + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluatorOutputDataType, + EvaluatorOutputFieldDefinition, + PublicEvaluatorOutputDefinition_Numeric, + ) + + PublicEvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the answer is correct or incorrect.", + ), + score=EvaluatorOutputFieldDefinition( + description="Return a score between 0 and 1.", + ), + ) + """ + + data_type: typing_extensions.Annotated[ + typing.Literal["NUMERIC"], FieldMetadata(alias="dataType") + ] = "NUMERIC" + reasoning: EvaluatorOutputFieldDefinition + score: EvaluatorOutputFieldDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class PublicEvaluatorOutputDefinition_Boolean(UniversalBaseModel): + """ + Evaluator output definition returned by the public API. + + This response always includes `dataType` and never includes an internal output-definition `version`. + Legacy stored evaluator definitions are normalized into this shape before they are returned. + + Use this response shape when deciding how to interpret future evaluation scores: + - `NUMERIC`: expect numeric score values + - `BOOLEAN`: expect `true` / `false` + - `CATEGORICAL`: expect one or more values from `score.categories` + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluatorOutputDataType, + EvaluatorOutputFieldDefinition, + PublicEvaluatorOutputDefinition_Numeric, + ) + + PublicEvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the answer is correct or incorrect.", + ), + score=EvaluatorOutputFieldDefinition( + description="Return a score between 0 and 1.", + ), + ) + """ + + data_type: typing_extensions.Annotated[ + typing.Literal["BOOLEAN"], FieldMetadata(alias="dataType") + ] = "BOOLEAN" + reasoning: EvaluatorOutputFieldDefinition + score: EvaluatorOutputFieldDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class PublicEvaluatorOutputDefinition_Categorical(UniversalBaseModel): + """ + Evaluator output definition returned by the public API. + + This response always includes `dataType` and never includes an internal output-definition `version`. + Legacy stored evaluator definitions are normalized into this shape before they are returned. + + Use this response shape when deciding how to interpret future evaluation scores: + - `NUMERIC`: expect numeric score values + - `BOOLEAN`: expect `true` / `false` + - `CATEGORICAL`: expect one or more values from `score.categories` + + Examples + -------- + from langfuse.unstable.commons import ( + EvaluatorOutputDataType, + EvaluatorOutputFieldDefinition, + PublicEvaluatorOutputDefinition_Numeric, + ) + + PublicEvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the answer is correct or incorrect.", + ), + score=EvaluatorOutputFieldDefinition( + description="Return a score between 0 and 1.", + ), + ) + """ + + data_type: typing_extensions.Annotated[ + typing.Literal["CATEGORICAL"], FieldMetadata(alias="dataType") + ] = "CATEGORICAL" + reasoning: EvaluatorOutputFieldDefinition + score: PublicCategoricalEvaluatorOutputScoreDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +""" +from langfuse.unstable.commons import ( + EvaluatorOutputDataType, + EvaluatorOutputFieldDefinition, + PublicEvaluatorOutputDefinition_Numeric, +) + +PublicEvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the answer is correct or incorrect.", + ), + score=EvaluatorOutputFieldDefinition( + description="Return a score between 0 and 1.", + ), +) +""" +PublicEvaluatorOutputDefinition = typing_extensions.Annotated[ + typing.Union[ + PublicEvaluatorOutputDefinition_Numeric, + PublicEvaluatorOutputDefinition_Boolean, + PublicEvaluatorOutputDefinition_Categorical, + ], + pydantic.Field(discriminator="data_type"), +] diff --git a/langfuse/api/unstable/commons/types/public_numeric_evaluator_output_definition.py b/langfuse/api/unstable/commons/types/public_numeric_evaluator_output_definition.py new file mode 100644 index 000000000..68987d2ff --- /dev/null +++ b/langfuse/api/unstable/commons/types/public_numeric_evaluator_output_definition.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .evaluator_output_data_type import EvaluatorOutputDataType +from .evaluator_output_field_definition import EvaluatorOutputFieldDefinition + + +class PublicNumericEvaluatorOutputDefinition(UniversalBaseModel): + data_type: typing_extensions.Annotated[ + EvaluatorOutputDataType, FieldMetadata(alias="dataType") + ] = pydantic.Field() + """ + Always `NUMERIC`. + """ + + reasoning: EvaluatorOutputFieldDefinition + score: EvaluatorOutputFieldDefinition + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/string_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/string_evaluation_rule_filter.py new file mode 100644 index 000000000..bd9332092 --- /dev/null +++ b/langfuse/api/unstable/commons/types/string_evaluation_rule_filter.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_string_filter_operator import EvaluationRuleStringFilterOperator + + +class StringEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Column to filter on. + """ + + operator: EvaluationRuleStringFilterOperator + value: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/string_object_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/string_object_evaluation_rule_filter.py new file mode 100644 index 000000000..6c287aad6 --- /dev/null +++ b/langfuse/api/unstable/commons/types/string_object_evaluation_rule_filter.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_string_filter_operator import EvaluationRuleStringFilterOperator + + +class StringObjectEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Object-valued column to filter on. In the unstable public API this is currently `metadata`. + """ + + key: str = pydantic.Field() + """ + Top-level key inside the object-valued column to filter on. + """ + + operator: EvaluationRuleStringFilterOperator + value: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/commons/types/string_options_evaluation_rule_filter.py b/langfuse/api/unstable/commons/types/string_options_evaluation_rule_filter.py new file mode 100644 index 000000000..a830e5ad9 --- /dev/null +++ b/langfuse/api/unstable/commons/types/string_options_evaluation_rule_filter.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .evaluation_rule_options_filter_operator import EvaluationRuleOptionsFilterOperator + + +class StringOptionsEvaluationRuleFilter(UniversalBaseModel): + column: str = pydantic.Field() + """ + Column to filter on. + """ + + operator: EvaluationRuleOptionsFilterOperator + value: typing.List[str] = pydantic.Field() + """ + One or more allowed string values. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/errors/__init__.py b/langfuse/api/unstable/errors/__init__.py new file mode 100644 index 000000000..42f230c41 --- /dev/null +++ b/langfuse/api/unstable/errors/__init__.py @@ -0,0 +1,84 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + PublicApiError, + PublicApiErrorCode, + PublicApiErrorDetails, + PublicApiValidationIssue, + ) + from .errors import ( + AccessDeniedError, + BadRequestError, + ConflictError, + InternalServerError, + MethodNotAllowedError, + NotFoundError, + TooManyRequestsError, + UnauthorizedError, + UnprocessableContentError, + ) +_dynamic_imports: typing.Dict[str, str] = { + "AccessDeniedError": ".errors", + "BadRequestError": ".errors", + "ConflictError": ".errors", + "InternalServerError": ".errors", + "MethodNotAllowedError": ".errors", + "NotFoundError": ".errors", + "PublicApiError": ".types", + "PublicApiErrorCode": ".types", + "PublicApiErrorDetails": ".types", + "PublicApiValidationIssue": ".types", + "TooManyRequestsError": ".errors", + "UnauthorizedError": ".errors", + "UnprocessableContentError": ".errors", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AccessDeniedError", + "BadRequestError", + "ConflictError", + "InternalServerError", + "MethodNotAllowedError", + "NotFoundError", + "PublicApiError", + "PublicApiErrorCode", + "PublicApiErrorDetails", + "PublicApiValidationIssue", + "TooManyRequestsError", + "UnauthorizedError", + "UnprocessableContentError", +] diff --git a/langfuse/api/unstable/errors/errors/__init__.py b/langfuse/api/unstable/errors/errors/__init__.py new file mode 100644 index 000000000..510e3beb1 --- /dev/null +++ b/langfuse/api/unstable/errors/errors/__init__.py @@ -0,0 +1,68 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .access_denied_error import AccessDeniedError + from .bad_request_error import BadRequestError + from .conflict_error import ConflictError + from .internal_server_error import InternalServerError + from .method_not_allowed_error import MethodNotAllowedError + from .not_found_error import NotFoundError + from .too_many_requests_error import TooManyRequestsError + from .unauthorized_error import UnauthorizedError + from .unprocessable_content_error import UnprocessableContentError +_dynamic_imports: typing.Dict[str, str] = { + "AccessDeniedError": ".access_denied_error", + "BadRequestError": ".bad_request_error", + "ConflictError": ".conflict_error", + "InternalServerError": ".internal_server_error", + "MethodNotAllowedError": ".method_not_allowed_error", + "NotFoundError": ".not_found_error", + "TooManyRequestsError": ".too_many_requests_error", + "UnauthorizedError": ".unauthorized_error", + "UnprocessableContentError": ".unprocessable_content_error", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AccessDeniedError", + "BadRequestError", + "ConflictError", + "InternalServerError", + "MethodNotAllowedError", + "NotFoundError", + "TooManyRequestsError", + "UnauthorizedError", + "UnprocessableContentError", +] diff --git a/langfuse/api/unstable/errors/errors/access_denied_error.py b/langfuse/api/unstable/errors/errors/access_denied_error.py new file mode 100644 index 000000000..6e07b4c79 --- /dev/null +++ b/langfuse/api/unstable/errors/errors/access_denied_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class AccessDeniedError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=403, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/errors/bad_request_error.py b/langfuse/api/unstable/errors/errors/bad_request_error.py new file mode 100644 index 000000000..7ba4c1a00 --- /dev/null +++ b/langfuse/api/unstable/errors/errors/bad_request_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class BadRequestError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/errors/conflict_error.py b/langfuse/api/unstable/errors/errors/conflict_error.py new file mode 100644 index 000000000..3630eec67 --- /dev/null +++ b/langfuse/api/unstable/errors/errors/conflict_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class ConflictError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=409, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/errors/internal_server_error.py b/langfuse/api/unstable/errors/errors/internal_server_error.py new file mode 100644 index 000000000..5921a86ae --- /dev/null +++ b/langfuse/api/unstable/errors/errors/internal_server_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class InternalServerError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=500, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/errors/method_not_allowed_error.py b/langfuse/api/unstable/errors/errors/method_not_allowed_error.py new file mode 100644 index 000000000..547598806 --- /dev/null +++ b/langfuse/api/unstable/errors/errors/method_not_allowed_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class MethodNotAllowedError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=405, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/errors/not_found_error.py b/langfuse/api/unstable/errors/errors/not_found_error.py new file mode 100644 index 000000000..1b65b230e --- /dev/null +++ b/langfuse/api/unstable/errors/errors/not_found_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class NotFoundError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/errors/too_many_requests_error.py b/langfuse/api/unstable/errors/errors/too_many_requests_error.py new file mode 100644 index 000000000..2a8345bc7 --- /dev/null +++ b/langfuse/api/unstable/errors/errors/too_many_requests_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class TooManyRequestsError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=429, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/errors/unauthorized_error.py b/langfuse/api/unstable/errors/errors/unauthorized_error.py new file mode 100644 index 000000000..84d847643 --- /dev/null +++ b/langfuse/api/unstable/errors/errors/unauthorized_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class UnauthorizedError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/errors/unprocessable_content_error.py b/langfuse/api/unstable/errors/errors/unprocessable_content_error.py new file mode 100644 index 000000000..a701ef9c5 --- /dev/null +++ b/langfuse/api/unstable/errors/errors/unprocessable_content_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core.api_error import ApiError +from ..types.public_api_error import PublicApiError + + +class UnprocessableContentError(ApiError): + def __init__( + self, + body: PublicApiError, + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=422, headers=headers, body=body) diff --git a/langfuse/api/unstable/errors/types/__init__.py b/langfuse/api/unstable/errors/types/__init__.py new file mode 100644 index 000000000..fd016304e --- /dev/null +++ b/langfuse/api/unstable/errors/types/__init__.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .public_api_error import PublicApiError + from .public_api_error_code import PublicApiErrorCode + from .public_api_error_details import PublicApiErrorDetails + from .public_api_validation_issue import PublicApiValidationIssue +_dynamic_imports: typing.Dict[str, str] = { + "PublicApiError": ".public_api_error", + "PublicApiErrorCode": ".public_api_error_code", + "PublicApiErrorDetails": ".public_api_error_details", + "PublicApiValidationIssue": ".public_api_validation_issue", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "PublicApiError", + "PublicApiErrorCode", + "PublicApiErrorDetails", + "PublicApiValidationIssue", +] diff --git a/langfuse/api/unstable/errors/types/public_api_error.py b/langfuse/api/unstable/errors/types/public_api_error.py new file mode 100644 index 000000000..5d1384e7c --- /dev/null +++ b/langfuse/api/unstable/errors/types/public_api_error.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .public_api_error_code import PublicApiErrorCode +from .public_api_error_details import PublicApiErrorDetails + + +class PublicApiError(UniversalBaseModel): + """ + Standard error envelope for the unstable evaluators API. + + Response handling guidance: + - Use the HTTP status code for the broad class of failure. + - Use `code` for precise branching in SDKs, CLIs, or agents. + - Inspect `details` for field-level validation context such as invalid filter values, malformed JSONPath expressions, or missing variable mappings. + - Retry only after fixing the specific issue described by `code` and `details`. + + Examples + -------- + from langfuse.unstable.errors import ( + PublicApiError, + PublicApiErrorCode, + PublicApiErrorDetails, + ) + + PublicApiError( + message='Filter column "type" contains unsupported value(s): INVALID', + code=PublicApiErrorCode.INVALID_FILTER_VALUE, + details=PublicApiErrorDetails( + field="filter[0].value", + column="type", + invalid_values=["INVALID"], + allowed_values=["GENERATION", "SPAN", "EVENT"], + ), + ) + """ + + message: str = pydantic.Field() + """ + Human-readable description of the failure. + """ + + code: PublicApiErrorCode = pydantic.Field() + """ + Stable machine-readable error code. + """ + + details: typing.Optional[PublicApiErrorDetails] = pydantic.Field(default=None) + """ + Optional structured error context. Inspect the populated fields based on `code`. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/errors/types/public_api_error_code.py b/langfuse/api/unstable/errors/types/public_api_error_code.py new file mode 100644 index 000000000..fe8f67f83 --- /dev/null +++ b/langfuse/api/unstable/errors/types/public_api_error_code.py @@ -0,0 +1,93 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class PublicApiErrorCode(enum.StrEnum): + """ + Machine-readable error code returned by the unstable evaluators API. + + SDKs, CLIs, and agents should branch on `code` rather than parsing the human-readable `message`. + The HTTP status still indicates the broad error class, while `code` gives the specific failure reason. + """ + + AUTHENTICATION_FAILED = "authentication_failed" + ACCESS_DENIED = "access_denied" + INVALID_REQUEST = "invalid_request" + INVALID_QUERY = "invalid_query" + INVALID_BODY = "invalid_body" + INVALID_FILTER_VALUE = "invalid_filter_value" + INVALID_JSON_PATH = "invalid_json_path" + INVALID_VARIABLE_MAPPING = "invalid_variable_mapping" + MISSING_VARIABLE_MAPPING = "missing_variable_mapping" + DUPLICATE_VARIABLE_MAPPING = "duplicate_variable_mapping" + RESOURCE_NOT_FOUND = "resource_not_found" + NAME_CONFLICT = "name_conflict" + EVALUATOR_PREFLIGHT_FAILED = "evaluator_preflight_failed" + CONFLICT = "conflict" + UNPROCESSABLE_CONTENT = "unprocessable_content" + RATE_LIMITED = "rate_limited" + METHOD_NOT_ALLOWED = "method_not_allowed" + INTERNAL_ERROR = "internal_error" + + def visit( + self, + authentication_failed: typing.Callable[[], T_Result], + access_denied: typing.Callable[[], T_Result], + invalid_request: typing.Callable[[], T_Result], + invalid_query: typing.Callable[[], T_Result], + invalid_body: typing.Callable[[], T_Result], + invalid_filter_value: typing.Callable[[], T_Result], + invalid_json_path: typing.Callable[[], T_Result], + invalid_variable_mapping: typing.Callable[[], T_Result], + missing_variable_mapping: typing.Callable[[], T_Result], + duplicate_variable_mapping: typing.Callable[[], T_Result], + resource_not_found: typing.Callable[[], T_Result], + name_conflict: typing.Callable[[], T_Result], + evaluator_preflight_failed: typing.Callable[[], T_Result], + conflict: typing.Callable[[], T_Result], + unprocessable_content: typing.Callable[[], T_Result], + rate_limited: typing.Callable[[], T_Result], + method_not_allowed: typing.Callable[[], T_Result], + internal_error: typing.Callable[[], T_Result], + ) -> T_Result: + if self is PublicApiErrorCode.AUTHENTICATION_FAILED: + return authentication_failed() + if self is PublicApiErrorCode.ACCESS_DENIED: + return access_denied() + if self is PublicApiErrorCode.INVALID_REQUEST: + return invalid_request() + if self is PublicApiErrorCode.INVALID_QUERY: + return invalid_query() + if self is PublicApiErrorCode.INVALID_BODY: + return invalid_body() + if self is PublicApiErrorCode.INVALID_FILTER_VALUE: + return invalid_filter_value() + if self is PublicApiErrorCode.INVALID_JSON_PATH: + return invalid_json_path() + if self is PublicApiErrorCode.INVALID_VARIABLE_MAPPING: + return invalid_variable_mapping() + if self is PublicApiErrorCode.MISSING_VARIABLE_MAPPING: + return missing_variable_mapping() + if self is PublicApiErrorCode.DUPLICATE_VARIABLE_MAPPING: + return duplicate_variable_mapping() + if self is PublicApiErrorCode.RESOURCE_NOT_FOUND: + return resource_not_found() + if self is PublicApiErrorCode.NAME_CONFLICT: + return name_conflict() + if self is PublicApiErrorCode.EVALUATOR_PREFLIGHT_FAILED: + return evaluator_preflight_failed() + if self is PublicApiErrorCode.CONFLICT: + return conflict() + if self is PublicApiErrorCode.UNPROCESSABLE_CONTENT: + return unprocessable_content() + if self is PublicApiErrorCode.RATE_LIMITED: + return rate_limited() + if self is PublicApiErrorCode.METHOD_NOT_ALLOWED: + return method_not_allowed() + if self is PublicApiErrorCode.INTERNAL_ERROR: + return internal_error() diff --git a/langfuse/api/unstable/errors/types/public_api_error_details.py b/langfuse/api/unstable/errors/types/public_api_error_details.py new file mode 100644 index 000000000..803378164 --- /dev/null +++ b/langfuse/api/unstable/errors/types/public_api_error_details.py @@ -0,0 +1,114 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .public_api_validation_issue import PublicApiValidationIssue + + +class PublicApiErrorDetails(UniversalBaseModel): + """ + Optional structured context attached to an unstable-evals error. + + The populated fields depend on the error `code`: + - request parsing failures populate `issues` + - filter validation failures populate `field`, `column`, `invalidValues`, and `allowedValues` + - variable mapping failures populate `field`, `variable`, or `variables` + - JSONPath validation failures populate `field`, `variable`, and `value` + - evaluator preflight failures populate `evaluatorName`, `provider`, and `model` + - rate limiting populates `retryAfterSeconds`, `limit`, `remaining`, and `resetAt` + """ + + issues: typing.Optional[typing.List[PublicApiValidationIssue]] = pydantic.Field( + default=None + ) + """ + Validation issues for malformed request bodies or query parameters. + """ + + field: typing.Optional[str] = pydantic.Field(default=None) + """ + Path-like reference to the failing field, for example `mapping[1].jsonPath`. + """ + + column: typing.Optional[str] = pydantic.Field(default=None) + """ + Filter column that failed validation. + """ + + invalid_values: typing_extensions.Annotated[ + typing.Optional[typing.List[str]], FieldMetadata(alias="invalidValues") + ] = pydantic.Field(default=None) + """ + Unsupported values supplied by the caller. + """ + + allowed_values: typing_extensions.Annotated[ + typing.Optional[typing.List[str]], FieldMetadata(alias="allowedValues") + ] = pydantic.Field(default=None) + """ + Allowed values for the failing filter column. + """ + + variable: typing.Optional[str] = pydantic.Field(default=None) + """ + Evaluator variable involved in the failure. + """ + + variables: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Multiple evaluator variables involved in the failure, for example missing mappings. + """ + + value: typing.Optional[str] = pydantic.Field(default=None) + """ + Raw invalid value supplied by the caller. + """ + + evaluator_name: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="evaluatorName") + ] = pydantic.Field(default=None) + """ + Evaluator name used during preflight validation. + """ + + provider: typing.Optional[str] = pydantic.Field(default=None) + """ + Provider resolved during evaluator preflight, if any. + """ + + model: typing.Optional[str] = pydantic.Field(default=None) + """ + Model resolved during evaluator preflight, if any. + """ + + retry_after_seconds: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="retryAfterSeconds") + ] = pydantic.Field(default=None) + """ + Suggested retry delay for rate-limited requests. + """ + + limit: typing.Optional[int] = pydantic.Field(default=None) + """ + Numeric limit associated with the failure, for example the active evaluation-rule cap or the current rate-limit window. + """ + + remaining: typing.Optional[int] = pydantic.Field(default=None) + """ + Remaining requests in the current rate-limit window. + """ + + reset_at: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="resetAt") + ] = pydantic.Field(default=None) + """ + ISO-8601 timestamp when the current rate-limit window resets. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/errors/types/public_api_validation_issue.py b/langfuse/api/unstable/errors/types/public_api_validation_issue.py new file mode 100644 index 000000000..877d0376a --- /dev/null +++ b/langfuse/api/unstable/errors/types/public_api_validation_issue.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel + + +class PublicApiValidationIssue(UniversalBaseModel): + """ + One validation issue returned for malformed request bodies or query parameters. + + This mirrors the most important parts of a Zod issue: a machine-readable `code`, + a human-readable `message`, and a structured `path`. + """ + + code: str = pydantic.Field() + """ + Machine-readable validation issue code emitted by the server validator. + """ + + message: str = pydantic.Field() + """ + Human-readable explanation of the validation failure. + """ + + path: typing.List[typing.Any] = pydantic.Field() + """ + Path to the invalid field, for example `["mapping", 0, "jsonPath"]`. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/__init__.py b/langfuse/api/unstable/evaluation_rules/__init__.py new file mode 100644 index 000000000..8541bdcc8 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/__init__.py @@ -0,0 +1,79 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + CodeEvaluationRuleEvaluatorReference, + CreateCodeEvaluationRuleRequest, + CreateEvaluationRuleRequest, + CreateLlmAsJudgeEvaluationRuleRequest, + DeleteEvaluationRuleResponse, + EvaluationRule, + EvaluationRuleEvaluator, + EvaluationRuleEvaluatorReference, + EvaluationRules, + LlmAsJudgeEvaluationRuleEvaluatorReference, + LlmAsJudgeEvaluatorType, + UpdateEvaluationRuleRequest, + ) +_dynamic_imports: typing.Dict[str, str] = { + "CodeEvaluationRuleEvaluatorReference": ".types", + "CreateCodeEvaluationRuleRequest": ".types", + "CreateEvaluationRuleRequest": ".types", + "CreateLlmAsJudgeEvaluationRuleRequest": ".types", + "DeleteEvaluationRuleResponse": ".types", + "EvaluationRule": ".types", + "EvaluationRuleEvaluator": ".types", + "EvaluationRuleEvaluatorReference": ".types", + "EvaluationRules": ".types", + "LlmAsJudgeEvaluationRuleEvaluatorReference": ".types", + "LlmAsJudgeEvaluatorType": ".types", + "UpdateEvaluationRuleRequest": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CodeEvaluationRuleEvaluatorReference", + "CreateCodeEvaluationRuleRequest", + "CreateEvaluationRuleRequest", + "CreateLlmAsJudgeEvaluationRuleRequest", + "DeleteEvaluationRuleResponse", + "EvaluationRule", + "EvaluationRuleEvaluator", + "EvaluationRuleEvaluatorReference", + "EvaluationRules", + "LlmAsJudgeEvaluationRuleEvaluatorReference", + "LlmAsJudgeEvaluatorType", + "UpdateEvaluationRuleRequest", +] diff --git a/langfuse/api/unstable/evaluation_rules/client.py b/langfuse/api/unstable/evaluation_rules/client.py new file mode 100644 index 000000000..aa0cefbdf --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/client.py @@ -0,0 +1,800 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ..commons.types.evaluation_rule_filter import EvaluationRuleFilter +from ..commons.types.evaluation_rule_mapping import EvaluationRuleMapping +from ..commons.types.evaluation_rule_target import EvaluationRuleTarget +from .raw_client import AsyncRawEvaluationRulesClient, RawEvaluationRulesClient +from .types.create_evaluation_rule_request import CreateEvaluationRuleRequest +from .types.delete_evaluation_rule_response import DeleteEvaluationRuleResponse +from .types.evaluation_rule import EvaluationRule +from .types.evaluation_rule_evaluator_reference import EvaluationRuleEvaluatorReference +from .types.evaluation_rules import EvaluationRules + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class EvaluationRulesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawEvaluationRulesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawEvaluationRulesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawEvaluationRulesClient + """ + return self._raw_client + + def create( + self, + *, + request: CreateEvaluationRuleRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> EvaluationRule: + """ + Create an evaluation rule. + + An evaluation rule defines **what** incoming data should be evaluated and **how prompt variables should be populated** from that data. + + Use this resource after choosing an evaluator from the evaluator endpoints. + + Key rules: + - `name` must be unique within the project for public evaluation rules + - `target` must be `observation` or `experiment` + - `evaluator.name` + `evaluator.scope` must identify an existing evaluator family returned by the evaluator endpoints + - Langfuse resolves that family to its latest version before saving the evaluation rule + - for `target=experiment`, use dataset `id` values from `GET /api/public/v2/datasets` when filtering by `datasetId` + - for `llm_as_judge` evaluators, every evaluator prompt variable must be mapped exactly once + - for `code` evaluators, Langfuse uses the fixed code runtime mapping; omit `mapping` in create and update requests + - for user-provided `llm_as_judge` mappings, `expected_output` and `experiment_item_metadata` are only valid for `target=experiment` + - if `enabled=true`, Langfuse validates that the referenced evaluator can currently run + - at most 50 evaluation rules can be effectively active in one project at the same time + + If an evaluation rule with the same `name` already exists in the project, the API returns `409`. + In that case, update the existing resource with `PATCH /api/public/unstable/evaluation-rules/{evaluationRuleId}` instead of creating a second one. + + If enabling this resource would exceed the 50-active limit, the API also returns `409`. + In that case, disable or pause another active evaluation rule before enabling a new one. + + Current scope: + - evaluation rules are live-ingestion rules only + - they do not trigger historical backfills + + Recovery guidance: + - `400 invalid_filter_value`: fix the filter `column` or `value` using `details.column`, `details.invalidValues`, and `details.allowedValues` + - `400 invalid_filter_value` with `details.column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response + - `400 missing_variable_mapping`: for `llm_as_judge` evaluators, fetch the evaluator again and make sure every variable in `variables` appears exactly once in `mapping` + - `400 duplicate_variable_mapping`: remove repeated mappings for the same variable + - `400 invalid_variable_mapping`: for `llm_as_judge`, switch to a valid `source` for the selected `target`, or fix the variable name + - `400 invalid_json_path`: remove or correct the `jsonPath` + - `422 evaluator_preflight_failed`: the selected evaluator cannot run with the resolved model configuration. Fix the evaluator/default model setup, then retry the create request. + + Parameters + ---------- + request : CreateEvaluationRuleRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EvaluationRule + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleMapping, + EvaluationRuleMappingSource, + EvaluationRuleOptionsFilterOperator, + EvaluationRuleTarget, + EvaluatorScope, + ) + from langfuse.unstable.evaluation_rules import ( + CreateLlmAsJudgeEvaluationRuleRequest, + LlmAsJudgeEvaluationRuleEvaluatorReference, + LlmAsJudgeEvaluatorType, + ) + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.evaluation_rules.create( + request=CreateLlmAsJudgeEvaluationRuleRequest( + name="answer-correctness-live", + evaluator=LlmAsJudgeEvaluationRuleEvaluatorReference( + name="answer-correctness", + scope=EvaluatorScope.PROJECT, + type=LlmAsJudgeEvaluatorType.LLM_AS_JUDGE, + ), + target=EvaluationRuleTarget.OBSERVATION, + enabled=True, + sampling=1.0, + filter=[ + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + ], + mapping=[ + EvaluationRuleMapping( + variable="input", + source=EvaluationRuleMappingSource.INPUT, + ), + EvaluationRuleMapping( + variable="output", + source=EvaluationRuleMappingSource.OUTPUT, + ), + ], + ), + ) + """ + _response = self._raw_client.create( + request=request, request_options=request_options + ) + return _response.data + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> EvaluationRules: + """ + List evaluation rules in the authenticated project. + + Each item describes one live evaluation rule and its effective runtime status. + + Parameters + ---------- + page : typing.Optional[int] + 1-based page number. Defaults to `1`. + + limit : typing.Optional[int] + Maximum number of items per page. Defaults to `50`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EvaluationRules + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.evaluation_rules.list() + """ + _response = self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + def get( + self, + evaluation_rule_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> EvaluationRule: + """ + Get one evaluation rule by its identifier. + + Use this endpoint to inspect the current evaluator, target, mapping, filters, and effective runtime status. + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier returned by the evaluation rule endpoints. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EvaluationRule + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.evaluation_rules.get( + evaluation_rule_id="evaluationRuleId", + ) + """ + _response = self._raw_client.get( + evaluation_rule_id, request_options=request_options + ) + return _response.data + + def update( + self, + evaluation_rule_id: str, + *, + name: typing.Optional[str] = OMIT, + evaluator: typing.Optional[EvaluationRuleEvaluatorReference] = OMIT, + target: typing.Optional[EvaluationRuleTarget] = OMIT, + enabled: typing.Optional[bool] = OMIT, + sampling: typing.Optional[float] = OMIT, + filter: typing.Optional[typing.Sequence[EvaluationRuleFilter]] = OMIT, + mapping: typing.Optional[typing.Sequence[EvaluationRuleMapping]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> EvaluationRule: + """ + Update an evaluation rule. + + Typical uses: + - enable or disable live execution + - switch to another evaluator + - adjust sampling + - change filters + - update LLM-as-judge variable mappings + + Important behavior: + - provide only the fields you want to change + - if you provide `evaluator`, Langfuse resolves that evaluator family to its latest version before saving + - changing `target`, `filter`, or an LLM-as-judge `mapping` must still produce a valid target-specific configuration + - if you change `target` for an LLM-as-judge rule, also send a compatible `filter` and `mapping` in the same request unless the existing ones are still valid for the new target + - for `code` evaluator rules, omit `mapping`; Langfuse stores the fixed code runtime mapping automatically + - if the resulting config is enabled, Langfuse re-validates that the selected evaluator can run + - if the update would move a non-active evaluation rule into the active state and the project already has 50 active evaluation rules, the API returns `409` + + Recovery guidance: + - if an LLM-as-judge update fails with `missing_variable_mapping` or `invalid_variable_mapping` after changing `evaluator` or `target`, resend the request with a complete new `mapping` + - if the update fails with `invalid_filter_value` after changing `target`, resend the request with a target-compatible `filter` + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier. + + name : typing.Optional[str] + Updated deployment name. + + evaluator : typing.Optional[EvaluationRuleEvaluatorReference] + Updated evaluator family. + + Langfuse resolves the provided evaluator family to its latest version before saving the rule. + A rule's evaluator type cannot be changed: provide `name` and `scope` for an evaluator family of the rule's current type. To use a different evaluator type, create a new rule. + + target : typing.Optional[EvaluationRuleTarget] + Updated target object type. + + enabled : typing.Optional[bool] + Updated desired enabled state. + + sampling : typing.Optional[float] + Updated sampling fraction. + + filter : typing.Optional[typing.Sequence[EvaluationRuleFilter]] + Updated filter list. + + For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + mapping : typing.Optional[typing.Sequence[EvaluationRuleMapping]] + Updated LLM-as-judge variable mappings. + + Do not send this field for code evaluator rules. Langfuse stores the fixed code runtime mapping automatically and returns it in the response. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EvaluationRule + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.evaluation_rules.update( + evaluation_rule_id="evaluationRuleId", + ) + """ + _response = self._raw_client.update( + evaluation_rule_id, + name=name, + evaluator=evaluator, + target=target, + enabled=enabled, + sampling=sampling, + filter=filter, + mapping=mapping, + request_options=request_options, + ) + return _response.data + + def delete( + self, + evaluation_rule_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteEvaluationRuleResponse: + """ + Delete an evaluation rule. + + This removes the live-ingestion rule only. It does not delete the referenced evaluator. + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteEvaluationRuleResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.evaluation_rules.delete( + evaluation_rule_id="evaluationRuleId", + ) + """ + _response = self._raw_client.delete( + evaluation_rule_id, request_options=request_options + ) + return _response.data + + +class AsyncEvaluationRulesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawEvaluationRulesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawEvaluationRulesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawEvaluationRulesClient + """ + return self._raw_client + + async def create( + self, + *, + request: CreateEvaluationRuleRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> EvaluationRule: + """ + Create an evaluation rule. + + An evaluation rule defines **what** incoming data should be evaluated and **how prompt variables should be populated** from that data. + + Use this resource after choosing an evaluator from the evaluator endpoints. + + Key rules: + - `name` must be unique within the project for public evaluation rules + - `target` must be `observation` or `experiment` + - `evaluator.name` + `evaluator.scope` must identify an existing evaluator family returned by the evaluator endpoints + - Langfuse resolves that family to its latest version before saving the evaluation rule + - for `target=experiment`, use dataset `id` values from `GET /api/public/v2/datasets` when filtering by `datasetId` + - for `llm_as_judge` evaluators, every evaluator prompt variable must be mapped exactly once + - for `code` evaluators, Langfuse uses the fixed code runtime mapping; omit `mapping` in create and update requests + - for user-provided `llm_as_judge` mappings, `expected_output` and `experiment_item_metadata` are only valid for `target=experiment` + - if `enabled=true`, Langfuse validates that the referenced evaluator can currently run + - at most 50 evaluation rules can be effectively active in one project at the same time + + If an evaluation rule with the same `name` already exists in the project, the API returns `409`. + In that case, update the existing resource with `PATCH /api/public/unstable/evaluation-rules/{evaluationRuleId}` instead of creating a second one. + + If enabling this resource would exceed the 50-active limit, the API also returns `409`. + In that case, disable or pause another active evaluation rule before enabling a new one. + + Current scope: + - evaluation rules are live-ingestion rules only + - they do not trigger historical backfills + + Recovery guidance: + - `400 invalid_filter_value`: fix the filter `column` or `value` using `details.column`, `details.invalidValues`, and `details.allowedValues` + - `400 invalid_filter_value` with `details.column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response + - `400 missing_variable_mapping`: for `llm_as_judge` evaluators, fetch the evaluator again and make sure every variable in `variables` appears exactly once in `mapping` + - `400 duplicate_variable_mapping`: remove repeated mappings for the same variable + - `400 invalid_variable_mapping`: for `llm_as_judge`, switch to a valid `source` for the selected `target`, or fix the variable name + - `400 invalid_json_path`: remove or correct the `jsonPath` + - `422 evaluator_preflight_failed`: the selected evaluator cannot run with the resolved model configuration. Fix the evaluator/default model setup, then retry the create request. + + Parameters + ---------- + request : CreateEvaluationRuleRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EvaluationRule + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleMapping, + EvaluationRuleMappingSource, + EvaluationRuleOptionsFilterOperator, + EvaluationRuleTarget, + EvaluatorScope, + ) + from langfuse.unstable.evaluation_rules import ( + CreateLlmAsJudgeEvaluationRuleRequest, + LlmAsJudgeEvaluationRuleEvaluatorReference, + LlmAsJudgeEvaluatorType, + ) + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.evaluation_rules.create( + request=CreateLlmAsJudgeEvaluationRuleRequest( + name="answer-correctness-live", + evaluator=LlmAsJudgeEvaluationRuleEvaluatorReference( + name="answer-correctness", + scope=EvaluatorScope.PROJECT, + type=LlmAsJudgeEvaluatorType.LLM_AS_JUDGE, + ), + target=EvaluationRuleTarget.OBSERVATION, + enabled=True, + sampling=1.0, + filter=[ + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + ], + mapping=[ + EvaluationRuleMapping( + variable="input", + source=EvaluationRuleMappingSource.INPUT, + ), + EvaluationRuleMapping( + variable="output", + source=EvaluationRuleMappingSource.OUTPUT, + ), + ], + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + request=request, request_options=request_options + ) + return _response.data + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> EvaluationRules: + """ + List evaluation rules in the authenticated project. + + Each item describes one live evaluation rule and its effective runtime status. + + Parameters + ---------- + page : typing.Optional[int] + 1-based page number. Defaults to `1`. + + limit : typing.Optional[int] + Maximum number of items per page. Defaults to `50`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EvaluationRules + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.evaluation_rules.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def get( + self, + evaluation_rule_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> EvaluationRule: + """ + Get one evaluation rule by its identifier. + + Use this endpoint to inspect the current evaluator, target, mapping, filters, and effective runtime status. + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier returned by the evaluation rule endpoints. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EvaluationRule + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.evaluation_rules.get( + evaluation_rule_id="evaluationRuleId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + evaluation_rule_id, request_options=request_options + ) + return _response.data + + async def update( + self, + evaluation_rule_id: str, + *, + name: typing.Optional[str] = OMIT, + evaluator: typing.Optional[EvaluationRuleEvaluatorReference] = OMIT, + target: typing.Optional[EvaluationRuleTarget] = OMIT, + enabled: typing.Optional[bool] = OMIT, + sampling: typing.Optional[float] = OMIT, + filter: typing.Optional[typing.Sequence[EvaluationRuleFilter]] = OMIT, + mapping: typing.Optional[typing.Sequence[EvaluationRuleMapping]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> EvaluationRule: + """ + Update an evaluation rule. + + Typical uses: + - enable or disable live execution + - switch to another evaluator + - adjust sampling + - change filters + - update LLM-as-judge variable mappings + + Important behavior: + - provide only the fields you want to change + - if you provide `evaluator`, Langfuse resolves that evaluator family to its latest version before saving + - changing `target`, `filter`, or an LLM-as-judge `mapping` must still produce a valid target-specific configuration + - if you change `target` for an LLM-as-judge rule, also send a compatible `filter` and `mapping` in the same request unless the existing ones are still valid for the new target + - for `code` evaluator rules, omit `mapping`; Langfuse stores the fixed code runtime mapping automatically + - if the resulting config is enabled, Langfuse re-validates that the selected evaluator can run + - if the update would move a non-active evaluation rule into the active state and the project already has 50 active evaluation rules, the API returns `409` + + Recovery guidance: + - if an LLM-as-judge update fails with `missing_variable_mapping` or `invalid_variable_mapping` after changing `evaluator` or `target`, resend the request with a complete new `mapping` + - if the update fails with `invalid_filter_value` after changing `target`, resend the request with a target-compatible `filter` + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier. + + name : typing.Optional[str] + Updated deployment name. + + evaluator : typing.Optional[EvaluationRuleEvaluatorReference] + Updated evaluator family. + + Langfuse resolves the provided evaluator family to its latest version before saving the rule. + A rule's evaluator type cannot be changed: provide `name` and `scope` for an evaluator family of the rule's current type. To use a different evaluator type, create a new rule. + + target : typing.Optional[EvaluationRuleTarget] + Updated target object type. + + enabled : typing.Optional[bool] + Updated desired enabled state. + + sampling : typing.Optional[float] + Updated sampling fraction. + + filter : typing.Optional[typing.Sequence[EvaluationRuleFilter]] + Updated filter list. + + For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + mapping : typing.Optional[typing.Sequence[EvaluationRuleMapping]] + Updated LLM-as-judge variable mappings. + + Do not send this field for code evaluator rules. Langfuse stores the fixed code runtime mapping automatically and returns it in the response. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + EvaluationRule + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.evaluation_rules.update( + evaluation_rule_id="evaluationRuleId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + evaluation_rule_id, + name=name, + evaluator=evaluator, + target=target, + enabled=enabled, + sampling=sampling, + filter=filter, + mapping=mapping, + request_options=request_options, + ) + return _response.data + + async def delete( + self, + evaluation_rule_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteEvaluationRuleResponse: + """ + Delete an evaluation rule. + + This removes the live-ingestion rule only. It does not delete the referenced evaluator. + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteEvaluationRuleResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.evaluation_rules.delete( + evaluation_rule_id="evaluationRuleId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete( + evaluation_rule_id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/unstable/evaluation_rules/raw_client.py b/langfuse/api/unstable/evaluation_rules/raw_client.py new file mode 100644 index 000000000..7115cbe70 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/raw_client.py @@ -0,0 +1,2180 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...commons.errors.access_denied_error import ( + AccessDeniedError as commons_errors_access_denied_error_AccessDeniedError, +) +from ...commons.errors.error import Error +from ...commons.errors.method_not_allowed_error import ( + MethodNotAllowedError as commons_errors_method_not_allowed_error_MethodNotAllowedError, +) +from ...commons.errors.not_found_error import ( + NotFoundError as commons_errors_not_found_error_NotFoundError, +) +from ...commons.errors.unauthorized_error import ( + UnauthorizedError as commons_errors_unauthorized_error_UnauthorizedError, +) +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import jsonable_encoder +from ...core.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from ..commons.types.evaluation_rule_filter import EvaluationRuleFilter +from ..commons.types.evaluation_rule_mapping import EvaluationRuleMapping +from ..commons.types.evaluation_rule_target import EvaluationRuleTarget +from ..errors.errors.access_denied_error import ( + AccessDeniedError as unstable_errors_errors_access_denied_error_AccessDeniedError, +) +from ..errors.errors.bad_request_error import BadRequestError +from ..errors.errors.conflict_error import ConflictError +from ..errors.errors.internal_server_error import InternalServerError +from ..errors.errors.method_not_allowed_error import ( + MethodNotAllowedError as unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError, +) +from ..errors.errors.not_found_error import ( + NotFoundError as unstable_errors_errors_not_found_error_NotFoundError, +) +from ..errors.errors.too_many_requests_error import TooManyRequestsError +from ..errors.errors.unauthorized_error import ( + UnauthorizedError as unstable_errors_errors_unauthorized_error_UnauthorizedError, +) +from ..errors.errors.unprocessable_content_error import UnprocessableContentError +from ..errors.types.public_api_error import PublicApiError +from .types.create_evaluation_rule_request import CreateEvaluationRuleRequest +from .types.delete_evaluation_rule_response import DeleteEvaluationRuleResponse +from .types.evaluation_rule import EvaluationRule +from .types.evaluation_rule_evaluator_reference import EvaluationRuleEvaluatorReference +from .types.evaluation_rules import EvaluationRules + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawEvaluationRulesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + request: CreateEvaluationRuleRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[EvaluationRule]: + """ + Create an evaluation rule. + + An evaluation rule defines **what** incoming data should be evaluated and **how prompt variables should be populated** from that data. + + Use this resource after choosing an evaluator from the evaluator endpoints. + + Key rules: + - `name` must be unique within the project for public evaluation rules + - `target` must be `observation` or `experiment` + - `evaluator.name` + `evaluator.scope` must identify an existing evaluator family returned by the evaluator endpoints + - Langfuse resolves that family to its latest version before saving the evaluation rule + - for `target=experiment`, use dataset `id` values from `GET /api/public/v2/datasets` when filtering by `datasetId` + - for `llm_as_judge` evaluators, every evaluator prompt variable must be mapped exactly once + - for `code` evaluators, Langfuse uses the fixed code runtime mapping; omit `mapping` in create and update requests + - for user-provided `llm_as_judge` mappings, `expected_output` and `experiment_item_metadata` are only valid for `target=experiment` + - if `enabled=true`, Langfuse validates that the referenced evaluator can currently run + - at most 50 evaluation rules can be effectively active in one project at the same time + + If an evaluation rule with the same `name` already exists in the project, the API returns `409`. + In that case, update the existing resource with `PATCH /api/public/unstable/evaluation-rules/{evaluationRuleId}` instead of creating a second one. + + If enabling this resource would exceed the 50-active limit, the API also returns `409`. + In that case, disable or pause another active evaluation rule before enabling a new one. + + Current scope: + - evaluation rules are live-ingestion rules only + - they do not trigger historical backfills + + Recovery guidance: + - `400 invalid_filter_value`: fix the filter `column` or `value` using `details.column`, `details.invalidValues`, and `details.allowedValues` + - `400 invalid_filter_value` with `details.column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response + - `400 missing_variable_mapping`: for `llm_as_judge` evaluators, fetch the evaluator again and make sure every variable in `variables` appears exactly once in `mapping` + - `400 duplicate_variable_mapping`: remove repeated mappings for the same variable + - `400 invalid_variable_mapping`: for `llm_as_judge`, switch to a valid `source` for the selected `target`, or fix the variable name + - `400 invalid_json_path`: remove or correct the `jsonPath` + - `422 evaluator_preflight_failed`: the selected evaluator cannot run with the resolved model configuration. Fix the evaluator/default model setup, then retry the create request. + + Parameters + ---------- + request : CreateEvaluationRuleRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[EvaluationRule] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/unstable/evaluation-rules", + method="POST", + json=convert_and_respect_annotation_metadata( + object_=request, + annotation=CreateEvaluationRuleRequest, + direction="write", + ), + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EvaluationRule, + parse_obj_as( + type_=EvaluationRule, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableContentError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[EvaluationRules]: + """ + List evaluation rules in the authenticated project. + + Each item describes one live evaluation rule and its effective runtime status. + + Parameters + ---------- + page : typing.Optional[int] + 1-based page number. Defaults to `1`. + + limit : typing.Optional[int] + Maximum number of items per page. Defaults to `50`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[EvaluationRules] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/unstable/evaluation-rules", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EvaluationRules, + parse_obj_as( + type_=EvaluationRules, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, + evaluation_rule_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[EvaluationRule]: + """ + Get one evaluation rule by its identifier. + + Use this endpoint to inspect the current evaluator, target, mapping, filters, and effective runtime status. + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier returned by the evaluation rule endpoints. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[EvaluationRule] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/unstable/evaluation-rules/{jsonable_encoder(evaluation_rule_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EvaluationRule, + parse_obj_as( + type_=EvaluationRule, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def update( + self, + evaluation_rule_id: str, + *, + name: typing.Optional[str] = OMIT, + evaluator: typing.Optional[EvaluationRuleEvaluatorReference] = OMIT, + target: typing.Optional[EvaluationRuleTarget] = OMIT, + enabled: typing.Optional[bool] = OMIT, + sampling: typing.Optional[float] = OMIT, + filter: typing.Optional[typing.Sequence[EvaluationRuleFilter]] = OMIT, + mapping: typing.Optional[typing.Sequence[EvaluationRuleMapping]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[EvaluationRule]: + """ + Update an evaluation rule. + + Typical uses: + - enable or disable live execution + - switch to another evaluator + - adjust sampling + - change filters + - update LLM-as-judge variable mappings + + Important behavior: + - provide only the fields you want to change + - if you provide `evaluator`, Langfuse resolves that evaluator family to its latest version before saving + - changing `target`, `filter`, or an LLM-as-judge `mapping` must still produce a valid target-specific configuration + - if you change `target` for an LLM-as-judge rule, also send a compatible `filter` and `mapping` in the same request unless the existing ones are still valid for the new target + - for `code` evaluator rules, omit `mapping`; Langfuse stores the fixed code runtime mapping automatically + - if the resulting config is enabled, Langfuse re-validates that the selected evaluator can run + - if the update would move a non-active evaluation rule into the active state and the project already has 50 active evaluation rules, the API returns `409` + + Recovery guidance: + - if an LLM-as-judge update fails with `missing_variable_mapping` or `invalid_variable_mapping` after changing `evaluator` or `target`, resend the request with a complete new `mapping` + - if the update fails with `invalid_filter_value` after changing `target`, resend the request with a target-compatible `filter` + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier. + + name : typing.Optional[str] + Updated deployment name. + + evaluator : typing.Optional[EvaluationRuleEvaluatorReference] + Updated evaluator family. + + Langfuse resolves the provided evaluator family to its latest version before saving the rule. + A rule's evaluator type cannot be changed: provide `name` and `scope` for an evaluator family of the rule's current type. To use a different evaluator type, create a new rule. + + target : typing.Optional[EvaluationRuleTarget] + Updated target object type. + + enabled : typing.Optional[bool] + Updated desired enabled state. + + sampling : typing.Optional[float] + Updated sampling fraction. + + filter : typing.Optional[typing.Sequence[EvaluationRuleFilter]] + Updated filter list. + + For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + mapping : typing.Optional[typing.Sequence[EvaluationRuleMapping]] + Updated LLM-as-judge variable mappings. + + Do not send this field for code evaluator rules. Langfuse stores the fixed code runtime mapping automatically and returns it in the response. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[EvaluationRule] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/unstable/evaluation-rules/{jsonable_encoder(evaluation_rule_id)}", + method="PATCH", + json={ + "name": name, + "evaluator": convert_and_respect_annotation_metadata( + object_=evaluator, + annotation=EvaluationRuleEvaluatorReference, + direction="write", + ), + "target": target, + "enabled": enabled, + "sampling": sampling, + "filter": convert_and_respect_annotation_metadata( + object_=filter, + annotation=typing.Sequence[EvaluationRuleFilter], + direction="write", + ), + "mapping": convert_and_respect_annotation_metadata( + object_=mapping, + annotation=typing.Sequence[EvaluationRuleMapping], + direction="write", + ), + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EvaluationRule, + parse_obj_as( + type_=EvaluationRule, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableContentError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete( + self, + evaluation_rule_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DeleteEvaluationRuleResponse]: + """ + Delete an evaluation rule. + + This removes the live-ingestion rule only. It does not delete the referenced evaluator. + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteEvaluationRuleResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/unstable/evaluation-rules/{jsonable_encoder(evaluation_rule_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteEvaluationRuleResponse, + parse_obj_as( + type_=DeleteEvaluationRuleResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawEvaluationRulesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + request: CreateEvaluationRuleRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[EvaluationRule]: + """ + Create an evaluation rule. + + An evaluation rule defines **what** incoming data should be evaluated and **how prompt variables should be populated** from that data. + + Use this resource after choosing an evaluator from the evaluator endpoints. + + Key rules: + - `name` must be unique within the project for public evaluation rules + - `target` must be `observation` or `experiment` + - `evaluator.name` + `evaluator.scope` must identify an existing evaluator family returned by the evaluator endpoints + - Langfuse resolves that family to its latest version before saving the evaluation rule + - for `target=experiment`, use dataset `id` values from `GET /api/public/v2/datasets` when filtering by `datasetId` + - for `llm_as_judge` evaluators, every evaluator prompt variable must be mapped exactly once + - for `code` evaluators, Langfuse uses the fixed code runtime mapping; omit `mapping` in create and update requests + - for user-provided `llm_as_judge` mappings, `expected_output` and `experiment_item_metadata` are only valid for `target=experiment` + - if `enabled=true`, Langfuse validates that the referenced evaluator can currently run + - at most 50 evaluation rules can be effectively active in one project at the same time + + If an evaluation rule with the same `name` already exists in the project, the API returns `409`. + In that case, update the existing resource with `PATCH /api/public/unstable/evaluation-rules/{evaluationRuleId}` instead of creating a second one. + + If enabling this resource would exceed the 50-active limit, the API also returns `409`. + In that case, disable or pause another active evaluation rule before enabling a new one. + + Current scope: + - evaluation rules are live-ingestion rules only + - they do not trigger historical backfills + + Recovery guidance: + - `400 invalid_filter_value`: fix the filter `column` or `value` using `details.column`, `details.invalidValues`, and `details.allowedValues` + - `400 invalid_filter_value` with `details.column=datasetId`: call `GET /api/public/v2/datasets`, then retry with dataset `id` values from that response + - `400 missing_variable_mapping`: for `llm_as_judge` evaluators, fetch the evaluator again and make sure every variable in `variables` appears exactly once in `mapping` + - `400 duplicate_variable_mapping`: remove repeated mappings for the same variable + - `400 invalid_variable_mapping`: for `llm_as_judge`, switch to a valid `source` for the selected `target`, or fix the variable name + - `400 invalid_json_path`: remove or correct the `jsonPath` + - `422 evaluator_preflight_failed`: the selected evaluator cannot run with the resolved model configuration. Fix the evaluator/default model setup, then retry the create request. + + Parameters + ---------- + request : CreateEvaluationRuleRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[EvaluationRule] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/unstable/evaluation-rules", + method="POST", + json=convert_and_respect_annotation_metadata( + object_=request, + annotation=CreateEvaluationRuleRequest, + direction="write", + ), + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EvaluationRule, + parse_obj_as( + type_=EvaluationRule, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableContentError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[EvaluationRules]: + """ + List evaluation rules in the authenticated project. + + Each item describes one live evaluation rule and its effective runtime status. + + Parameters + ---------- + page : typing.Optional[int] + 1-based page number. Defaults to `1`. + + limit : typing.Optional[int] + Maximum number of items per page. Defaults to `50`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[EvaluationRules] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/unstable/evaluation-rules", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EvaluationRules, + parse_obj_as( + type_=EvaluationRules, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, + evaluation_rule_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[EvaluationRule]: + """ + Get one evaluation rule by its identifier. + + Use this endpoint to inspect the current evaluator, target, mapping, filters, and effective runtime status. + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier returned by the evaluation rule endpoints. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[EvaluationRule] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/unstable/evaluation-rules/{jsonable_encoder(evaluation_rule_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EvaluationRule, + parse_obj_as( + type_=EvaluationRule, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def update( + self, + evaluation_rule_id: str, + *, + name: typing.Optional[str] = OMIT, + evaluator: typing.Optional[EvaluationRuleEvaluatorReference] = OMIT, + target: typing.Optional[EvaluationRuleTarget] = OMIT, + enabled: typing.Optional[bool] = OMIT, + sampling: typing.Optional[float] = OMIT, + filter: typing.Optional[typing.Sequence[EvaluationRuleFilter]] = OMIT, + mapping: typing.Optional[typing.Sequence[EvaluationRuleMapping]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[EvaluationRule]: + """ + Update an evaluation rule. + + Typical uses: + - enable or disable live execution + - switch to another evaluator + - adjust sampling + - change filters + - update LLM-as-judge variable mappings + + Important behavior: + - provide only the fields you want to change + - if you provide `evaluator`, Langfuse resolves that evaluator family to its latest version before saving + - changing `target`, `filter`, or an LLM-as-judge `mapping` must still produce a valid target-specific configuration + - if you change `target` for an LLM-as-judge rule, also send a compatible `filter` and `mapping` in the same request unless the existing ones are still valid for the new target + - for `code` evaluator rules, omit `mapping`; Langfuse stores the fixed code runtime mapping automatically + - if the resulting config is enabled, Langfuse re-validates that the selected evaluator can run + - if the update would move a non-active evaluation rule into the active state and the project already has 50 active evaluation rules, the API returns `409` + + Recovery guidance: + - if an LLM-as-judge update fails with `missing_variable_mapping` or `invalid_variable_mapping` after changing `evaluator` or `target`, resend the request with a complete new `mapping` + - if the update fails with `invalid_filter_value` after changing `target`, resend the request with a target-compatible `filter` + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier. + + name : typing.Optional[str] + Updated deployment name. + + evaluator : typing.Optional[EvaluationRuleEvaluatorReference] + Updated evaluator family. + + Langfuse resolves the provided evaluator family to its latest version before saving the rule. + A rule's evaluator type cannot be changed: provide `name` and `scope` for an evaluator family of the rule's current type. To use a different evaluator type, create a new rule. + + target : typing.Optional[EvaluationRuleTarget] + Updated target object type. + + enabled : typing.Optional[bool] + Updated desired enabled state. + + sampling : typing.Optional[float] + Updated sampling fraction. + + filter : typing.Optional[typing.Sequence[EvaluationRuleFilter]] + Updated filter list. + + For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + + mapping : typing.Optional[typing.Sequence[EvaluationRuleMapping]] + Updated LLM-as-judge variable mappings. + + Do not send this field for code evaluator rules. Langfuse stores the fixed code runtime mapping automatically and returns it in the response. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[EvaluationRule] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/unstable/evaluation-rules/{jsonable_encoder(evaluation_rule_id)}", + method="PATCH", + json={ + "name": name, + "evaluator": convert_and_respect_annotation_metadata( + object_=evaluator, + annotation=EvaluationRuleEvaluatorReference, + direction="write", + ), + "target": target, + "enabled": enabled, + "sampling": sampling, + "filter": convert_and_respect_annotation_metadata( + object_=filter, + annotation=typing.Sequence[EvaluationRuleFilter], + direction="write", + ), + "mapping": convert_and_respect_annotation_metadata( + object_=mapping, + annotation=typing.Sequence[EvaluationRuleMapping], + direction="write", + ), + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + EvaluationRule, + parse_obj_as( + type_=EvaluationRule, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableContentError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete( + self, + evaluation_rule_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DeleteEvaluationRuleResponse]: + """ + Delete an evaluation rule. + + This removes the live-ingestion rule only. It does not delete the referenced evaluator. + + Parameters + ---------- + evaluation_rule_id : str + Evaluation rule identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteEvaluationRuleResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/unstable/evaluation-rules/{jsonable_encoder(evaluation_rule_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteEvaluationRuleResponse, + parse_obj_as( + type_=DeleteEvaluationRuleResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/__init__.py b/langfuse/api/unstable/evaluation_rules/types/__init__.py new file mode 100644 index 000000000..a1cdeb967 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/__init__.py @@ -0,0 +1,83 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .code_evaluation_rule_evaluator_reference import ( + CodeEvaluationRuleEvaluatorReference, + ) + from .create_code_evaluation_rule_request import CreateCodeEvaluationRuleRequest + from .create_evaluation_rule_request import CreateEvaluationRuleRequest + from .create_llm_as_judge_evaluation_rule_request import ( + CreateLlmAsJudgeEvaluationRuleRequest, + ) + from .delete_evaluation_rule_response import DeleteEvaluationRuleResponse + from .evaluation_rule import EvaluationRule + from .evaluation_rule_evaluator import EvaluationRuleEvaluator + from .evaluation_rule_evaluator_reference import EvaluationRuleEvaluatorReference + from .evaluation_rules import EvaluationRules + from .llm_as_judge_evaluation_rule_evaluator_reference import ( + LlmAsJudgeEvaluationRuleEvaluatorReference, + ) + from .llm_as_judge_evaluator_type import LlmAsJudgeEvaluatorType + from .update_evaluation_rule_request import UpdateEvaluationRuleRequest +_dynamic_imports: typing.Dict[str, str] = { + "CodeEvaluationRuleEvaluatorReference": ".code_evaluation_rule_evaluator_reference", + "CreateCodeEvaluationRuleRequest": ".create_code_evaluation_rule_request", + "CreateEvaluationRuleRequest": ".create_evaluation_rule_request", + "CreateLlmAsJudgeEvaluationRuleRequest": ".create_llm_as_judge_evaluation_rule_request", + "DeleteEvaluationRuleResponse": ".delete_evaluation_rule_response", + "EvaluationRule": ".evaluation_rule", + "EvaluationRuleEvaluator": ".evaluation_rule_evaluator", + "EvaluationRuleEvaluatorReference": ".evaluation_rule_evaluator_reference", + "EvaluationRules": ".evaluation_rules", + "LlmAsJudgeEvaluationRuleEvaluatorReference": ".llm_as_judge_evaluation_rule_evaluator_reference", + "LlmAsJudgeEvaluatorType": ".llm_as_judge_evaluator_type", + "UpdateEvaluationRuleRequest": ".update_evaluation_rule_request", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CodeEvaluationRuleEvaluatorReference", + "CreateCodeEvaluationRuleRequest", + "CreateEvaluationRuleRequest", + "CreateLlmAsJudgeEvaluationRuleRequest", + "DeleteEvaluationRuleResponse", + "EvaluationRule", + "EvaluationRuleEvaluator", + "EvaluationRuleEvaluatorReference", + "EvaluationRules", + "LlmAsJudgeEvaluationRuleEvaluatorReference", + "LlmAsJudgeEvaluatorType", + "UpdateEvaluationRuleRequest", +] diff --git a/langfuse/api/unstable/evaluation_rules/types/code_evaluation_rule_evaluator_reference.py b/langfuse/api/unstable/evaluation_rules/types/code_evaluation_rule_evaluator_reference.py new file mode 100644 index 000000000..1c259bab8 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/code_evaluation_rule_evaluator_reference.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ...commons.types.evaluator_scope import EvaluatorScope + + +class CodeEvaluationRuleEvaluatorReference(UniversalBaseModel): + """ + Code evaluator family reference used when creating an evaluation rule. + """ + + name: str = pydantic.Field() + """ + Evaluator family name. + """ + + scope: EvaluatorScope = pydantic.Field() + """ + Whether the evaluator family is project-owned or Langfuse-managed. + """ + + type: typing.Literal["code"] = pydantic.Field(default="code") + """ + Must be `code`. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/create_code_evaluation_rule_request.py b/langfuse/api/unstable/evaluation_rules/types/create_code_evaluation_rule_request.py new file mode 100644 index 000000000..08df1f78a --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/create_code_evaluation_rule_request.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ...commons.types.evaluation_rule_filter import EvaluationRuleFilter +from ...commons.types.evaluation_rule_target import EvaluationRuleTarget +from .code_evaluation_rule_evaluator_reference import ( + CodeEvaluationRuleEvaluatorReference, +) + + +class CreateCodeEvaluationRuleRequest(UniversalBaseModel): + name: str = pydantic.Field() + """ + Human-readable deployment name. + """ + + evaluator: CodeEvaluationRuleEvaluatorReference = pydantic.Field() + """ + Code evaluator family to use. + + Use `name`, `scope`, and `type` from the evaluator endpoints. + Langfuse resolves that family to its latest version before saving the rule. + """ + + target: EvaluationRuleTarget = pydantic.Field() + """ + Target object type to evaluate. + """ + + enabled: bool = pydantic.Field() + """ + Whether the deployment should be active immediately after creation. + """ + + sampling: typing.Optional[float] = pydantic.Field(default=None) + """ + Optional sampling fraction. Defaults to `1`. + """ + + filter: typing.Optional[typing.List[EvaluationRuleFilter]] = pydantic.Field( + default=None + ) + """ + Optional filter list. + + Omit or pass an empty list to evaluate all matching targets for the selected `target`. + Each filter object must use a column that is valid for that `target`. + For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/create_evaluation_rule_request.py b/langfuse/api/unstable/evaluation_rules/types/create_evaluation_rule_request.py new file mode 100644 index 000000000..a6504934d --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/create_evaluation_rule_request.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .create_code_evaluation_rule_request import CreateCodeEvaluationRuleRequest +from .create_llm_as_judge_evaluation_rule_request import ( + CreateLlmAsJudgeEvaluationRuleRequest, +) + +CreateEvaluationRuleRequest = typing.Union[ + CreateLlmAsJudgeEvaluationRuleRequest, CreateCodeEvaluationRuleRequest +] diff --git a/langfuse/api/unstable/evaluation_rules/types/create_llm_as_judge_evaluation_rule_request.py b/langfuse/api/unstable/evaluation_rules/types/create_llm_as_judge_evaluation_rule_request.py new file mode 100644 index 000000000..b511b4353 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/create_llm_as_judge_evaluation_rule_request.py @@ -0,0 +1,65 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ...commons.types.evaluation_rule_filter import EvaluationRuleFilter +from ...commons.types.evaluation_rule_mapping import EvaluationRuleMapping +from ...commons.types.evaluation_rule_target import EvaluationRuleTarget +from .llm_as_judge_evaluation_rule_evaluator_reference import ( + LlmAsJudgeEvaluationRuleEvaluatorReference, +) + + +class CreateLlmAsJudgeEvaluationRuleRequest(UniversalBaseModel): + name: str = pydantic.Field() + """ + Human-readable deployment name. + """ + + evaluator: LlmAsJudgeEvaluationRuleEvaluatorReference = pydantic.Field() + """ + LLM-as-judge evaluator family to use. + + Use `name`, `scope`, and `type` from the evaluator endpoints. If `type` is omitted, Langfuse defaults it to `llm_as_judge` for backwards compatibility. + Langfuse resolves that family to its latest version before saving the rule. + """ + + target: EvaluationRuleTarget = pydantic.Field() + """ + Target object type to evaluate. + """ + + enabled: bool = pydantic.Field() + """ + Whether the deployment should be active immediately after creation. + """ + + sampling: typing.Optional[float] = pydantic.Field(default=None) + """ + Optional sampling fraction. Defaults to `1`. + """ + + filter: typing.Optional[typing.List[EvaluationRuleFilter]] = pydantic.Field( + default=None + ) + """ + Optional filter list. + + Omit or pass an empty list to evaluate all matching targets for the selected `target`. + Each filter object must use a column that is valid for that `target`. + For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + """ + + mapping: typing.List[EvaluationRuleMapping] = pydantic.Field() + """ + LLM-as-judge variable mappings. + + Every evaluator variable must appear exactly once. + Build this list from the evaluator `variables` array returned by the evaluator endpoints. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/delete_evaluation_rule_response.py b/langfuse/api/unstable/evaluation_rules/types/delete_evaluation_rule_response.py new file mode 100644 index 000000000..42423c3dc --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/delete_evaluation_rule_response.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel + + +class DeleteEvaluationRuleResponse(UniversalBaseModel): + """ + Confirmation response returned after successful deletion. + """ + + message: str = pydantic.Field() + """ + Always `Evaluation rule successfully deleted`. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/evaluation_rule.py b/langfuse/api/unstable/evaluation_rules/types/evaluation_rule.py new file mode 100644 index 000000000..418004090 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/evaluation_rule.py @@ -0,0 +1,174 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from ...commons.types.evaluation_rule_filter import EvaluationRuleFilter +from ...commons.types.evaluation_rule_mapping import EvaluationRuleMapping +from ...commons.types.evaluation_rule_status import EvaluationRuleStatus +from ...commons.types.evaluation_rule_target import EvaluationRuleTarget +from .evaluation_rule_evaluator import EvaluationRuleEvaluator + + +class EvaluationRule(UniversalBaseModel): + """ + Live evaluation rule for incoming data. + + An evaluation rule answers: + - which evaluator should be used + - which target objects should trigger scoring + - how often scoring should run + - which target fields should populate each evaluator variable + - whether the deployment is active, inactive, or paused + + Important status semantics: + - `enabled` is the desired on/off setting from the client + - `status` is the effective runtime state after Langfuse applies validation and blocking rules + - `enabled=true` with `status=paused` means the rule should run, but Langfuse has paused it until the underlying problem is fixed + + Examples + -------- + import datetime + + from langfuse.unstable.commons import ( + EvaluationRuleFilter_StringOptions, + EvaluationRuleMapping, + EvaluationRuleMappingSource, + EvaluationRuleOptionsFilterOperator, + EvaluationRuleStatus, + EvaluationRuleTarget, + EvaluatorScope, + EvaluatorType, + ) + from langfuse.unstable.evaluation_rules import ( + EvaluationRule, + EvaluationRuleEvaluator, + ) + + EvaluationRule( + id="erule_123", + name="answer-correctness-live", + evaluator=EvaluationRuleEvaluator( + id="evaltmpl_123", + name="answer-correctness", + scope=EvaluatorScope.PROJECT, + type=EvaluatorType.LLM_AS_JUDGE, + ), + target=EvaluationRuleTarget.OBSERVATION, + enabled=True, + status=EvaluationRuleStatus.ACTIVE, + sampling=1.0, + filter=[ + EvaluationRuleFilter_StringOptions( + column="type", + operator=EvaluationRuleOptionsFilterOperator.ANY_OF, + value=["GENERATION"], + ) + ], + mapping=[ + EvaluationRuleMapping( + variable="input", + source=EvaluationRuleMappingSource.INPUT, + ), + EvaluationRuleMapping( + variable="output", + source=EvaluationRuleMappingSource.OUTPUT, + ), + ], + created_at=datetime.datetime.fromisoformat( + "2026-03-30 09:20:00+00:00", + ), + updated_at=datetime.datetime.fromisoformat( + "2026-03-30 09:20:00+00:00", + ), + ) + """ + + id: str = pydantic.Field() + """ + Stable evaluation rule identifier. + """ + + name: str = pydantic.Field() + """ + Human-readable deployment name. This is independent from the evaluator name. + """ + + evaluator: EvaluationRuleEvaluator = pydantic.Field() + """ + Evaluator currently used by this rule. + + `name` and `scope` identify the evaluator family conceptually. + `id` is the currently active evaluator version in that family. + If you create a newer project version with the same evaluator name later, existing evaluation rules are moved to it automatically. + """ + + target: EvaluationRuleTarget = pydantic.Field() + """ + Target object type that should trigger scoring. + """ + + enabled: bool = pydantic.Field() + """ + Desired enabled state configured by the client. + """ + + status: EvaluationRuleStatus = pydantic.Field() + """ + Effective runtime status after Langfuse applies validation and blocking rules. + """ + + paused_reason: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="pausedReason") + ] = pydantic.Field(default=None) + """ + Machine-readable reason when `status=paused`, otherwise `null`. + """ + + paused_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="pausedMessage") + ] = pydantic.Field(default=None) + """ + Human-readable explanation when `status=paused`, otherwise `null`. + """ + + sampling: float = pydantic.Field() + """ + Fraction of matching target objects that should be evaluated. + + Must be greater than `0` and less than or equal to `1`. + - `1` means evaluate every matching target. + - `0.25` means evaluate approximately 25% of matching targets. + """ + + filter: typing.List[EvaluationRuleFilter] = pydantic.Field() + """ + List of filter conditions used to decide whether a target should be evaluated. + """ + + mapping: typing.List[EvaluationRuleMapping] = pydantic.Field() + """ + Variable mappings used to populate evaluator runtime variables from the live target object. + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] = pydantic.Field() + """ + Timestamp when the evaluation rule was created. + """ + + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] = pydantic.Field() + """ + Timestamp when the evaluation rule was last updated. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/evaluation_rule_evaluator.py b/langfuse/api/unstable/evaluation_rules/types/evaluation_rule_evaluator.py new file mode 100644 index 000000000..c27497c9d --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/evaluation_rule_evaluator.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ...commons.types.evaluator_scope import EvaluatorScope +from ...commons.types.evaluator_type import EvaluatorType + + +class EvaluationRuleEvaluator(UniversalBaseModel): + """ + Resolved evaluator currently used by the evaluation rule. + + `id` is the exact active evaluator version. + `name`, `scope`, and `type` identify the evaluator family conceptually. + """ + + id: str = pydantic.Field() + """ + Identifier of the exact evaluator version currently used by the rule. + """ + + name: str = pydantic.Field() + """ + Evaluator family name. + """ + + scope: EvaluatorScope = pydantic.Field() + """ + Whether the evaluator family is project-owned or Langfuse-managed. + """ + + type: EvaluatorType = pydantic.Field() + """ + Evaluator engine type. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/evaluation_rule_evaluator_reference.py b/langfuse/api/unstable/evaluation_rules/types/evaluation_rule_evaluator_reference.py new file mode 100644 index 000000000..a2a38723d --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/evaluation_rule_evaluator_reference.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ...commons.types.evaluator_scope import EvaluatorScope + + +class EvaluationRuleEvaluatorReference(UniversalBaseModel): + """ + Evaluator family reference used when updating an evaluation rule. + + `name` and `scope` identify the evaluator family in the authenticated project context. + A rule's evaluator type cannot be changed, so this reference does not accept a `type`; the family must match the rule's current evaluator type. + """ + + name: str = pydantic.Field() + """ + Evaluator family name. + """ + + scope: EvaluatorScope = pydantic.Field() + """ + Whether the evaluator family is project-owned or Langfuse-managed. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/evaluation_rules.py b/langfuse/api/unstable/evaluation_rules/types/evaluation_rules.py new file mode 100644 index 000000000..cd1f74c6d --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/evaluation_rules.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ....utils.pagination.types.meta_response import MetaResponse +from .evaluation_rule import EvaluationRule + + +class EvaluationRules(UniversalBaseModel): + """ + Paginated list of evaluation rules. + """ + + data: typing.List[EvaluationRule] = pydantic.Field() + """ + Evaluation rules in the current page. + """ + + meta: MetaResponse = pydantic.Field() + """ + Standard pagination metadata. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/llm_as_judge_evaluation_rule_evaluator_reference.py b/langfuse/api/unstable/evaluation_rules/types/llm_as_judge_evaluation_rule_evaluator_reference.py new file mode 100644 index 000000000..ca57fe517 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/llm_as_judge_evaluation_rule_evaluator_reference.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ...commons.types.evaluator_scope import EvaluatorScope +from .llm_as_judge_evaluator_type import LlmAsJudgeEvaluatorType + + +class LlmAsJudgeEvaluationRuleEvaluatorReference(UniversalBaseModel): + """ + LLM-as-judge evaluator family reference used when creating an evaluation rule. + """ + + name: str = pydantic.Field() + """ + Evaluator family name. + """ + + scope: EvaluatorScope = pydantic.Field() + """ + Whether the evaluator family is project-owned or Langfuse-managed. + """ + + type: typing.Optional[LlmAsJudgeEvaluatorType] = pydantic.Field(default=None) + """ + Evaluator engine type. Defaults to `llm_as_judge` when omitted. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluation_rules/types/llm_as_judge_evaluator_type.py b/langfuse/api/unstable/evaluation_rules/types/llm_as_judge_evaluator_type.py new file mode 100644 index 000000000..b18856d22 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/llm_as_judge_evaluator_type.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ....core import enum + +T_Result = typing.TypeVar("T_Result") + + +class LlmAsJudgeEvaluatorType(enum.StrEnum): + LLM_AS_JUDGE = "llm_as_judge" + + def visit(self, llm_as_judge: typing.Callable[[], T_Result]) -> T_Result: + if self is LlmAsJudgeEvaluatorType.LLM_AS_JUDGE: + return llm_as_judge() diff --git a/langfuse/api/unstable/evaluation_rules/types/update_evaluation_rule_request.py b/langfuse/api/unstable/evaluation_rules/types/update_evaluation_rule_request.py new file mode 100644 index 000000000..40e5043a6 --- /dev/null +++ b/langfuse/api/unstable/evaluation_rules/types/update_evaluation_rule_request.py @@ -0,0 +1,78 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ...commons.types.evaluation_rule_filter import EvaluationRuleFilter +from ...commons.types.evaluation_rule_mapping import EvaluationRuleMapping +from ...commons.types.evaluation_rule_target import EvaluationRuleTarget +from .evaluation_rule_evaluator_reference import EvaluationRuleEvaluatorReference + + +class UpdateEvaluationRuleRequest(UniversalBaseModel): + """ + Partial update body for an evaluation rule. + + Provide only the fields you want to change. + An empty body is rejected. + + Practical guidance: + - If you only want to rename the rule or change sampling, send just those fields. + - If you change to an LLM-as-judge `evaluator`, send a fresh `mapping` unless you are certain the existing mapping still matches the evaluator variables. + - If you change `target` for an LLM-as-judge rule, usually send both `filter` and `mapping` in the same request. + - For code evaluator rules, omit `mapping`; Langfuse stores the fixed code runtime mapping automatically. + - If you change an experiment `datasetId` filter, call `GET /api/public/v2/datasets` and use dataset `id` values from that response. + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Updated deployment name. + """ + + evaluator: typing.Optional[EvaluationRuleEvaluatorReference] = pydantic.Field( + default=None + ) + """ + Updated evaluator family. + + Langfuse resolves the provided evaluator family to its latest version before saving the rule. + A rule's evaluator type cannot be changed: provide `name` and `scope` for an evaluator family of the rule's current type. To use a different evaluator type, create a new rule. + """ + + target: typing.Optional[EvaluationRuleTarget] = pydantic.Field(default=None) + """ + Updated target object type. + """ + + enabled: typing.Optional[bool] = pydantic.Field(default=None) + """ + Updated desired enabled state. + """ + + sampling: typing.Optional[float] = pydantic.Field(default=None) + """ + Updated sampling fraction. + """ + + filter: typing.Optional[typing.List[EvaluationRuleFilter]] = pydantic.Field( + default=None + ) + """ + Updated filter list. + + For `target=experiment`, `column=datasetId` expects dataset `id` values from `GET /api/public/v2/datasets`, not dataset names. + """ + + mapping: typing.Optional[typing.List[EvaluationRuleMapping]] = pydantic.Field( + default=None + ) + """ + Updated LLM-as-judge variable mappings. + + Do not send this field for code evaluator rules. Langfuse stores the fixed code runtime mapping automatically and returns it in the response. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluators/__init__.py b/langfuse/api/unstable/evaluators/__init__.py new file mode 100644 index 000000000..20a72ef82 --- /dev/null +++ b/langfuse/api/unstable/evaluators/__init__.py @@ -0,0 +1,79 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + CodeEvaluator, + CreateCodeEvaluatorRequest, + CreateEvaluatorRequest, + CreateEvaluatorRequest_Code, + CreateEvaluatorRequest_LlmAsJudge, + CreateLlmAsJudgeEvaluatorRequest, + Evaluator, + EvaluatorBase, + Evaluator_Code, + Evaluator_LlmAsJudge, + Evaluators, + LlmAsJudgeEvaluator, + ) +_dynamic_imports: typing.Dict[str, str] = { + "CodeEvaluator": ".types", + "CreateCodeEvaluatorRequest": ".types", + "CreateEvaluatorRequest": ".types", + "CreateEvaluatorRequest_Code": ".types", + "CreateEvaluatorRequest_LlmAsJudge": ".types", + "CreateLlmAsJudgeEvaluatorRequest": ".types", + "Evaluator": ".types", + "EvaluatorBase": ".types", + "Evaluator_Code": ".types", + "Evaluator_LlmAsJudge": ".types", + "Evaluators": ".types", + "LlmAsJudgeEvaluator": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CodeEvaluator", + "CreateCodeEvaluatorRequest", + "CreateEvaluatorRequest", + "CreateEvaluatorRequest_Code", + "CreateEvaluatorRequest_LlmAsJudge", + "CreateLlmAsJudgeEvaluatorRequest", + "Evaluator", + "EvaluatorBase", + "Evaluator_Code", + "Evaluator_LlmAsJudge", + "Evaluators", + "LlmAsJudgeEvaluator", +] diff --git a/langfuse/api/unstable/evaluators/client.py b/langfuse/api/unstable/evaluators/client.py new file mode 100644 index 000000000..ac63e2da9 --- /dev/null +++ b/langfuse/api/unstable/evaluators/client.py @@ -0,0 +1,437 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from .raw_client import AsyncRawEvaluatorsClient, RawEvaluatorsClient +from .types.create_evaluator_request import CreateEvaluatorRequest +from .types.evaluator import Evaluator +from .types.evaluators import Evaluators + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class EvaluatorsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawEvaluatorsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawEvaluatorsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawEvaluatorsClient + """ + return self._raw_client + + def create( + self, + *, + request: CreateEvaluatorRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> Evaluator: + """ + Create an evaluator in the authenticated project. + + Use evaluators to define **how** Langfuse should score data. + LLM-as-a-judge evaluators define a prompt, expected structured output, and optional model configuration. + Code evaluators define source code and a runtime language. + + Naming behavior: + - If this is a new evaluator name in your project, Langfuse creates version `1`. + - If the name already exists in your project, Langfuse creates the next version and returns it. + - When a new project version is created, existing evaluation rules in that project automatically move to the newest version for that evaluator name. + + Recommended workflow: + 1. Create the evaluator. + 2. Read the returned `variables` array. + 3. Read the returned `outputDefinition.dataType` so the client knows whether future scores will be numeric, boolean, or categorical. + 4. Create one or more evaluation rules that reference the returned evaluator family using `name` and `scope`. + + Code evaluator validation: + - At creation, Langfuse only validates the request shape + - The `sourceCode` itself is not executed here. It is first run (preflight-tested against a sample observation) when you link the evaluator to an evaluation rule, so runtime errors in the code surface at evaluation-rule creation, not at evaluator creation. + + Recovery guidance: + - `422` with `code=evaluator_preflight_failed`: the evaluator cannot run with the resolved model configuration. Add a valid explicit `modelConfig`, or configure the project's default evaluation model, then retry the same request. + - `400` with `code=invalid_body`: the request shape is malformed. Use the structured `details.issues` array to fix the specific fields and retry. + - `400` with `code=invalid_body` on `outputDefinition`: for `type=llm_as_judge`, send `dataType`, `reasoning.description`, and `score.description`. Do not send `version`; it is not part of the public request shape. + - If `type` is omitted, Langfuse treats the request as `type=llm_as_judge` for backwards compatibility. New clients should send `type` explicitly. + + Unstable API note: + - This surface may evolve while the underlying evaluation data model is being redesigned. + + Parameters + ---------- + request : CreateEvaluatorRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Evaluator + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.unstable.commons import ( + EvaluatorModelConfig, + EvaluatorOutputDataType, + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputFieldDefinition, + ) + from langfuse.unstable.evaluators import CreateEvaluatorRequest_LlmAsJudge + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.evaluators.create( + request=CreateEvaluatorRequest_LlmAsJudge( + name="answer-correctness", + prompt="You are grading an answer.\n\nInput:\n{{input}}\n\nOutput:\n{{output}}\n\nReturn a score between 0 and 1.\n", + output_definition=EvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the score was assigned.", + ), + score=EvaluatorOutputFieldDefinition( + description="Correctness score between 0 and 1.", + ), + ), + model_config=EvaluatorModelConfig( + provider="openai", + model="gpt-4.1-mini", + ), + ), + ) + """ + _response = self._raw_client.create( + request=request, request_options=request_options + ) + return _response.data + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> Evaluators: + """ + List the evaluators available to the authenticated project. + + Important behavior: + - This endpoint returns the latest version of each available evaluator. + - Results can include evaluators from your project and Langfuse-managed evaluators. + - If the same evaluator name exists in both places, both are returned as separate items with different `scope` values. + + Parameters + ---------- + page : typing.Optional[int] + 1-based page number. Defaults to `1`. + + limit : typing.Optional[int] + Maximum number of items per page. Defaults to `50`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Evaluators + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.evaluators.list() + """ + _response = self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + def get( + self, + evaluator_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> Evaluator: + """ + Get one evaluator by `id`. + + Use this endpoint when you want the prompt, output definition, model configuration, and derived variables for the evaluator you plan to use in an evaluation rule. + + Parameters + ---------- + evaluator_id : str + Evaluator identifier returned by the evaluator endpoints. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Evaluator + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.evaluators.get( + evaluator_id="evaluatorId", + ) + """ + _response = self._raw_client.get(evaluator_id, request_options=request_options) + return _response.data + + +class AsyncEvaluatorsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawEvaluatorsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawEvaluatorsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawEvaluatorsClient + """ + return self._raw_client + + async def create( + self, + *, + request: CreateEvaluatorRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> Evaluator: + """ + Create an evaluator in the authenticated project. + + Use evaluators to define **how** Langfuse should score data. + LLM-as-a-judge evaluators define a prompt, expected structured output, and optional model configuration. + Code evaluators define source code and a runtime language. + + Naming behavior: + - If this is a new evaluator name in your project, Langfuse creates version `1`. + - If the name already exists in your project, Langfuse creates the next version and returns it. + - When a new project version is created, existing evaluation rules in that project automatically move to the newest version for that evaluator name. + + Recommended workflow: + 1. Create the evaluator. + 2. Read the returned `variables` array. + 3. Read the returned `outputDefinition.dataType` so the client knows whether future scores will be numeric, boolean, or categorical. + 4. Create one or more evaluation rules that reference the returned evaluator family using `name` and `scope`. + + Code evaluator validation: + - At creation, Langfuse only validates the request shape + - The `sourceCode` itself is not executed here. It is first run (preflight-tested against a sample observation) when you link the evaluator to an evaluation rule, so runtime errors in the code surface at evaluation-rule creation, not at evaluator creation. + + Recovery guidance: + - `422` with `code=evaluator_preflight_failed`: the evaluator cannot run with the resolved model configuration. Add a valid explicit `modelConfig`, or configure the project's default evaluation model, then retry the same request. + - `400` with `code=invalid_body`: the request shape is malformed. Use the structured `details.issues` array to fix the specific fields and retry. + - `400` with `code=invalid_body` on `outputDefinition`: for `type=llm_as_judge`, send `dataType`, `reasoning.description`, and `score.description`. Do not send `version`; it is not part of the public request shape. + - If `type` is omitted, Langfuse treats the request as `type=llm_as_judge` for backwards compatibility. New clients should send `type` explicitly. + + Unstable API note: + - This surface may evolve while the underlying evaluation data model is being redesigned. + + Parameters + ---------- + request : CreateEvaluatorRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Evaluator + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.unstable.commons import ( + EvaluatorModelConfig, + EvaluatorOutputDataType, + EvaluatorOutputDefinition_Numeric, + EvaluatorOutputFieldDefinition, + ) + from langfuse.unstable.evaluators import CreateEvaluatorRequest_LlmAsJudge + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.evaluators.create( + request=CreateEvaluatorRequest_LlmAsJudge( + name="answer-correctness", + prompt="You are grading an answer.\n\nInput:\n{{input}}\n\nOutput:\n{{output}}\n\nReturn a score between 0 and 1.\n", + output_definition=EvaluatorOutputDefinition_Numeric( + data_type=EvaluatorOutputDataType.NUMERIC, + reasoning=EvaluatorOutputFieldDefinition( + description="Explain why the score was assigned.", + ), + score=EvaluatorOutputFieldDefinition( + description="Correctness score between 0 and 1.", + ), + ), + model_config=EvaluatorModelConfig( + provider="openai", + model="gpt-4.1-mini", + ), + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + request=request, request_options=request_options + ) + return _response.data + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> Evaluators: + """ + List the evaluators available to the authenticated project. + + Important behavior: + - This endpoint returns the latest version of each available evaluator. + - Results can include evaluators from your project and Langfuse-managed evaluators. + - If the same evaluator name exists in both places, both are returned as separate items with different `scope` values. + + Parameters + ---------- + page : typing.Optional[int] + 1-based page number. Defaults to `1`. + + limit : typing.Optional[int] + Maximum number of items per page. Defaults to `50`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Evaluators + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.evaluators.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def get( + self, + evaluator_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> Evaluator: + """ + Get one evaluator by `id`. + + Use this endpoint when you want the prompt, output definition, model configuration, and derived variables for the evaluator you plan to use in an evaluation rule. + + Parameters + ---------- + evaluator_id : str + Evaluator identifier returned by the evaluator endpoints. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Evaluator + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.evaluators.get( + evaluator_id="evaluatorId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + evaluator_id, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/unstable/evaluators/raw_client.py b/langfuse/api/unstable/evaluators/raw_client.py new file mode 100644 index 000000000..30034d033 --- /dev/null +++ b/langfuse/api/unstable/evaluators/raw_client.py @@ -0,0 +1,1237 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...commons.errors.access_denied_error import ( + AccessDeniedError as commons_errors_access_denied_error_AccessDeniedError, +) +from ...commons.errors.error import Error +from ...commons.errors.method_not_allowed_error import ( + MethodNotAllowedError as commons_errors_method_not_allowed_error_MethodNotAllowedError, +) +from ...commons.errors.not_found_error import ( + NotFoundError as commons_errors_not_found_error_NotFoundError, +) +from ...commons.errors.unauthorized_error import ( + UnauthorizedError as commons_errors_unauthorized_error_UnauthorizedError, +) +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import jsonable_encoder +from ...core.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from ..errors.errors.access_denied_error import ( + AccessDeniedError as unstable_errors_errors_access_denied_error_AccessDeniedError, +) +from ..errors.errors.bad_request_error import BadRequestError +from ..errors.errors.conflict_error import ConflictError +from ..errors.errors.internal_server_error import InternalServerError +from ..errors.errors.method_not_allowed_error import ( + MethodNotAllowedError as unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError, +) +from ..errors.errors.not_found_error import ( + NotFoundError as unstable_errors_errors_not_found_error_NotFoundError, +) +from ..errors.errors.too_many_requests_error import TooManyRequestsError +from ..errors.errors.unauthorized_error import ( + UnauthorizedError as unstable_errors_errors_unauthorized_error_UnauthorizedError, +) +from ..errors.errors.unprocessable_content_error import UnprocessableContentError +from ..errors.types.public_api_error import PublicApiError +from .types.create_evaluator_request import CreateEvaluatorRequest +from .types.evaluator import Evaluator +from .types.evaluators import Evaluators + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawEvaluatorsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + request: CreateEvaluatorRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Evaluator]: + """ + Create an evaluator in the authenticated project. + + Use evaluators to define **how** Langfuse should score data. + LLM-as-a-judge evaluators define a prompt, expected structured output, and optional model configuration. + Code evaluators define source code and a runtime language. + + Naming behavior: + - If this is a new evaluator name in your project, Langfuse creates version `1`. + - If the name already exists in your project, Langfuse creates the next version and returns it. + - When a new project version is created, existing evaluation rules in that project automatically move to the newest version for that evaluator name. + + Recommended workflow: + 1. Create the evaluator. + 2. Read the returned `variables` array. + 3. Read the returned `outputDefinition.dataType` so the client knows whether future scores will be numeric, boolean, or categorical. + 4. Create one or more evaluation rules that reference the returned evaluator family using `name` and `scope`. + + Code evaluator validation: + - At creation, Langfuse only validates the request shape + - The `sourceCode` itself is not executed here. It is first run (preflight-tested against a sample observation) when you link the evaluator to an evaluation rule, so runtime errors in the code surface at evaluation-rule creation, not at evaluator creation. + + Recovery guidance: + - `422` with `code=evaluator_preflight_failed`: the evaluator cannot run with the resolved model configuration. Add a valid explicit `modelConfig`, or configure the project's default evaluation model, then retry the same request. + - `400` with `code=invalid_body`: the request shape is malformed. Use the structured `details.issues` array to fix the specific fields and retry. + - `400` with `code=invalid_body` on `outputDefinition`: for `type=llm_as_judge`, send `dataType`, `reasoning.description`, and `score.description`. Do not send `version`; it is not part of the public request shape. + - If `type` is omitted, Langfuse treats the request as `type=llm_as_judge` for backwards compatibility. New clients should send `type` explicitly. + + Unstable API note: + - This surface may evolve while the underlying evaluation data model is being redesigned. + + Parameters + ---------- + request : CreateEvaluatorRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Evaluator] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/unstable/evaluators", + method="POST", + json=convert_and_respect_annotation_metadata( + object_=request, annotation=CreateEvaluatorRequest, direction="write" + ), + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Evaluator, + parse_obj_as( + type_=Evaluator, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableContentError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Evaluators]: + """ + List the evaluators available to the authenticated project. + + Important behavior: + - This endpoint returns the latest version of each available evaluator. + - Results can include evaluators from your project and Langfuse-managed evaluators. + - If the same evaluator name exists in both places, both are returned as separate items with different `scope` values. + + Parameters + ---------- + page : typing.Optional[int] + 1-based page number. Defaults to `1`. + + limit : typing.Optional[int] + Maximum number of items per page. Defaults to `50`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Evaluators] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/unstable/evaluators", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Evaluators, + parse_obj_as( + type_=Evaluators, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, + evaluator_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[Evaluator]: + """ + Get one evaluator by `id`. + + Use this endpoint when you want the prompt, output definition, model configuration, and derived variables for the evaluator you plan to use in an evaluation rule. + + Parameters + ---------- + evaluator_id : str + Evaluator identifier returned by the evaluator endpoints. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Evaluator] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/unstable/evaluators/{jsonable_encoder(evaluator_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Evaluator, + parse_obj_as( + type_=Evaluator, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawEvaluatorsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + request: CreateEvaluatorRequest, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Evaluator]: + """ + Create an evaluator in the authenticated project. + + Use evaluators to define **how** Langfuse should score data. + LLM-as-a-judge evaluators define a prompt, expected structured output, and optional model configuration. + Code evaluators define source code and a runtime language. + + Naming behavior: + - If this is a new evaluator name in your project, Langfuse creates version `1`. + - If the name already exists in your project, Langfuse creates the next version and returns it. + - When a new project version is created, existing evaluation rules in that project automatically move to the newest version for that evaluator name. + + Recommended workflow: + 1. Create the evaluator. + 2. Read the returned `variables` array. + 3. Read the returned `outputDefinition.dataType` so the client knows whether future scores will be numeric, boolean, or categorical. + 4. Create one or more evaluation rules that reference the returned evaluator family using `name` and `scope`. + + Code evaluator validation: + - At creation, Langfuse only validates the request shape + - The `sourceCode` itself is not executed here. It is first run (preflight-tested against a sample observation) when you link the evaluator to an evaluation rule, so runtime errors in the code surface at evaluation-rule creation, not at evaluator creation. + + Recovery guidance: + - `422` with `code=evaluator_preflight_failed`: the evaluator cannot run with the resolved model configuration. Add a valid explicit `modelConfig`, or configure the project's default evaluation model, then retry the same request. + - `400` with `code=invalid_body`: the request shape is malformed. Use the structured `details.issues` array to fix the specific fields and retry. + - `400` with `code=invalid_body` on `outputDefinition`: for `type=llm_as_judge`, send `dataType`, `reasoning.description`, and `score.description`. Do not send `version`; it is not part of the public request shape. + - If `type` is omitted, Langfuse treats the request as `type=llm_as_judge` for backwards compatibility. New clients should send `type` explicitly. + + Unstable API note: + - This surface may evolve while the underlying evaluation data model is being redesigned. + + Parameters + ---------- + request : CreateEvaluatorRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Evaluator] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/unstable/evaluators", + method="POST", + json=convert_and_respect_annotation_metadata( + object_=request, annotation=CreateEvaluatorRequest, direction="write" + ), + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Evaluator, + parse_obj_as( + type_=Evaluator, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableContentError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def list( + self, + *, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Evaluators]: + """ + List the evaluators available to the authenticated project. + + Important behavior: + - This endpoint returns the latest version of each available evaluator. + - Results can include evaluators from your project and Langfuse-managed evaluators. + - If the same evaluator name exists in both places, both are returned as separate items with different `scope` values. + + Parameters + ---------- + page : typing.Optional[int] + 1-based page number. Defaults to `1`. + + limit : typing.Optional[int] + Maximum number of items per page. Defaults to `50`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Evaluators] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/unstable/evaluators", + method="GET", + params={ + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Evaluators, + parse_obj_as( + type_=Evaluators, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, + evaluator_id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[Evaluator]: + """ + Get one evaluator by `id`. + + Use this endpoint when you want the prompt, output definition, model configuration, and derived variables for the evaluator you plan to use in an evaluation rule. + + Parameters + ---------- + evaluator_id : str + Evaluator identifier returned by the evaluator endpoints. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Evaluator] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/unstable/evaluators/{jsonable_encoder(evaluator_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Evaluator, + parse_obj_as( + type_=Evaluator, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise unstable_errors_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise unstable_errors_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise unstable_errors_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise unstable_errors_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + PublicApiError, + parse_obj_as( + type_=PublicApiError, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise commons_errors_unauthorized_error_UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise commons_errors_access_denied_error_AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise commons_errors_method_not_allowed_error_MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise commons_errors_not_found_error_NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/unstable/evaluators/types/__init__.py b/langfuse/api/unstable/evaluators/types/__init__.py new file mode 100644 index 000000000..650598592 --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/__init__.py @@ -0,0 +1,77 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .code_evaluator import CodeEvaluator + from .create_code_evaluator_request import CreateCodeEvaluatorRequest + from .create_evaluator_request import ( + CreateEvaluatorRequest, + CreateEvaluatorRequest_Code, + CreateEvaluatorRequest_LlmAsJudge, + ) + from .create_llm_as_judge_evaluator_request import CreateLlmAsJudgeEvaluatorRequest + from .evaluator import Evaluator, Evaluator_Code, Evaluator_LlmAsJudge + from .evaluator_base import EvaluatorBase + from .evaluators import Evaluators + from .llm_as_judge_evaluator import LlmAsJudgeEvaluator +_dynamic_imports: typing.Dict[str, str] = { + "CodeEvaluator": ".code_evaluator", + "CreateCodeEvaluatorRequest": ".create_code_evaluator_request", + "CreateEvaluatorRequest": ".create_evaluator_request", + "CreateEvaluatorRequest_Code": ".create_evaluator_request", + "CreateEvaluatorRequest_LlmAsJudge": ".create_evaluator_request", + "CreateLlmAsJudgeEvaluatorRequest": ".create_llm_as_judge_evaluator_request", + "Evaluator": ".evaluator", + "EvaluatorBase": ".evaluator_base", + "Evaluator_Code": ".evaluator", + "Evaluator_LlmAsJudge": ".evaluator", + "Evaluators": ".evaluators", + "LlmAsJudgeEvaluator": ".llm_as_judge_evaluator", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CodeEvaluator", + "CreateCodeEvaluatorRequest", + "CreateEvaluatorRequest", + "CreateEvaluatorRequest_Code", + "CreateEvaluatorRequest_LlmAsJudge", + "CreateLlmAsJudgeEvaluatorRequest", + "Evaluator", + "EvaluatorBase", + "Evaluator_Code", + "Evaluator_LlmAsJudge", + "Evaluators", + "LlmAsJudgeEvaluator", +] diff --git a/langfuse/api/unstable/evaluators/types/code_evaluator.py b/langfuse/api/unstable/evaluators/types/code_evaluator.py new file mode 100644 index 000000000..f8648603d --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/code_evaluator.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.serialization import FieldMetadata +from ...commons.types.code_evaluator_source_code_language import ( + CodeEvaluatorSourceCodeLanguage, +) +from .evaluator_base import EvaluatorBase + + +class CodeEvaluator(EvaluatorBase): + source_code: typing_extensions.Annotated[str, FieldMetadata(alias="sourceCode")] = ( + pydantic.Field() + ) + """ + Source code executed for each matched observation. + """ + + source_code_language: typing_extensions.Annotated[ + CodeEvaluatorSourceCodeLanguage, FieldMetadata(alias="sourceCodeLanguage") + ] = pydantic.Field() + """ + Runtime language for `sourceCode`. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluators/types/create_code_evaluator_request.py b/langfuse/api/unstable/evaluators/types/create_code_evaluator_request.py new file mode 100644 index 000000000..860c15f9a --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/create_code_evaluator_request.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from ...commons.types.code_evaluator_source_code_language import ( + CodeEvaluatorSourceCodeLanguage, +) + + +class CreateCodeEvaluatorRequest(UniversalBaseModel): + name: str = pydantic.Field() + """ + Evaluator name within the authenticated project. + """ + + source_code: typing_extensions.Annotated[str, FieldMetadata(alias="sourceCode")] = ( + pydantic.Field() + ) + """ + Code executed for each matched observation. + """ + + source_code_language: typing_extensions.Annotated[ + CodeEvaluatorSourceCodeLanguage, FieldMetadata(alias="sourceCodeLanguage") + ] = pydantic.Field() + """ + Runtime language for `sourceCode`. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluators/types/create_evaluator_request.py b/langfuse/api/unstable/evaluators/types/create_evaluator_request.py new file mode 100644 index 000000000..a866aa4c5 --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/create_evaluator_request.py @@ -0,0 +1,66 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from ...commons.types.code_evaluator_source_code_language import ( + CodeEvaluatorSourceCodeLanguage, +) +from ...commons.types.evaluator_model_config import EvaluatorModelConfig +from ...commons.types.evaluator_output_definition import EvaluatorOutputDefinition + + +class CreateEvaluatorRequest_LlmAsJudge(UniversalBaseModel): + """ + Request body for creating an evaluator. + + If the same `name` already exists in your project, Langfuse creates the next version and returns it. + Existing evaluation rules in the same project are then moved to that new latest version automatically. + If `type` is omitted, Langfuse defaults it to `llm_as_judge` for backwards compatibility. + """ + + type: typing.Literal["llm_as_judge"] = "llm_as_judge" + name: str + prompt: str + output_definition: typing_extensions.Annotated[ + EvaluatorOutputDefinition, FieldMetadata(alias="outputDefinition") + ] + model_config_: typing_extensions.Annotated[ + typing.Optional[EvaluatorModelConfig], FieldMetadata(alias="modelConfig") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class CreateEvaluatorRequest_Code(UniversalBaseModel): + """ + Request body for creating an evaluator. + + If the same `name` already exists in your project, Langfuse creates the next version and returns it. + Existing evaluation rules in the same project are then moved to that new latest version automatically. + If `type` is omitted, Langfuse defaults it to `llm_as_judge` for backwards compatibility. + """ + + type: typing.Literal["code"] = "code" + name: str + source_code: typing_extensions.Annotated[str, FieldMetadata(alias="sourceCode")] + source_code_language: typing_extensions.Annotated[ + CodeEvaluatorSourceCodeLanguage, FieldMetadata(alias="sourceCodeLanguage") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +CreateEvaluatorRequest = typing_extensions.Annotated[ + typing.Union[CreateEvaluatorRequest_LlmAsJudge, CreateEvaluatorRequest_Code], + pydantic.Field(discriminator="type"), +] diff --git a/langfuse/api/unstable/evaluators/types/create_llm_as_judge_evaluator_request.py b/langfuse/api/unstable/evaluators/types/create_llm_as_judge_evaluator_request.py new file mode 100644 index 000000000..09e121b1b --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/create_llm_as_judge_evaluator_request.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from ...commons.types.evaluator_model_config import EvaluatorModelConfig +from ...commons.types.evaluator_output_definition import EvaluatorOutputDefinition + + +class CreateLlmAsJudgeEvaluatorRequest(UniversalBaseModel): + name: str = pydantic.Field() + """ + Evaluator name within the authenticated project. + """ + + prompt: str = pydantic.Field() + """ + Prompt template used by the evaluator. + """ + + output_definition: typing_extensions.Annotated[ + EvaluatorOutputDefinition, FieldMetadata(alias="outputDefinition") + ] = pydantic.Field() + """ + Structured output schema the evaluator must return. + + Always send `dataType`. + Do not send `version`; it is an internal storage detail and not part of the public request contract. + """ + + model_config_: typing_extensions.Annotated[ + typing.Optional[EvaluatorModelConfig], FieldMetadata(alias="modelConfig") + ] = pydantic.Field(default=None) + """ + Optional explicit model configuration. Omit or set to `null` to use the project default evaluation model. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluators/types/evaluator.py b/langfuse/api/unstable/evaluators/types/evaluator.py new file mode 100644 index 000000000..69295e0fd --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/evaluator.py @@ -0,0 +1,114 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from ...commons.types.code_evaluator_source_code_language import ( + CodeEvaluatorSourceCodeLanguage, +) +from ...commons.types.evaluator_model_config import EvaluatorModelConfig +from ...commons.types.evaluator_scope import EvaluatorScope +from ...commons.types.public_evaluator_output_definition import ( + PublicEvaluatorOutputDefinition, +) + + +class Evaluator_LlmAsJudge(UniversalBaseModel): + """ + One evaluator that can be used for scoring. + + An evaluator describes **how** to score data. + + It does not define **which** live objects are evaluated. That is the job of `evaluation-rules`. + + For agent clients, the most important fields are: + - `type`: determines which evaluator fields are present + - `variables`: for LLM evaluators, use these exact names when building the evaluation-rule `mapping` array. LLM evaluators require every variable to be mapped. Code evaluators always expose the fixed runtime payload fields and Langfuse maps them automatically. + + Versioning behavior: + - `GET /evaluators` returns the latest version of each available evaluator. + - `GET /evaluators/{id}` can return an older version. + - Evaluation rules always run against the latest version for the selected evaluator name within the same source (`project` or `managed`). + """ + + type: typing.Literal["llm_as_judge"] = "llm_as_judge" + prompt: str + output_definition: typing_extensions.Annotated[ + PublicEvaluatorOutputDefinition, FieldMetadata(alias="outputDefinition") + ] + model_config_: typing_extensions.Annotated[ + typing.Optional[EvaluatorModelConfig], FieldMetadata(alias="modelConfig") + ] = None + id: str + name: str + version: int + scope: EvaluatorScope + variables: typing.List[str] + evaluation_rule_count: typing_extensions.Annotated[ + int, FieldMetadata(alias="evaluationRuleCount") + ] + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +class Evaluator_Code(UniversalBaseModel): + """ + One evaluator that can be used for scoring. + + An evaluator describes **how** to score data. + + It does not define **which** live objects are evaluated. That is the job of `evaluation-rules`. + + For agent clients, the most important fields are: + - `type`: determines which evaluator fields are present + - `variables`: for LLM evaluators, use these exact names when building the evaluation-rule `mapping` array. LLM evaluators require every variable to be mapped. Code evaluators always expose the fixed runtime payload fields and Langfuse maps them automatically. + + Versioning behavior: + - `GET /evaluators` returns the latest version of each available evaluator. + - `GET /evaluators/{id}` can return an older version. + - Evaluation rules always run against the latest version for the selected evaluator name within the same source (`project` or `managed`). + """ + + type: typing.Literal["code"] = "code" + source_code: typing_extensions.Annotated[str, FieldMetadata(alias="sourceCode")] + source_code_language: typing_extensions.Annotated[ + CodeEvaluatorSourceCodeLanguage, FieldMetadata(alias="sourceCodeLanguage") + ] + id: str + name: str + version: int + scope: EvaluatorScope + variables: typing.List[str] + evaluation_rule_count: typing_extensions.Annotated[ + int, FieldMetadata(alias="evaluationRuleCount") + ] + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) + + +Evaluator = typing_extensions.Annotated[ + typing.Union[Evaluator_LlmAsJudge, Evaluator_Code], + pydantic.Field(discriminator="type"), +] diff --git a/langfuse/api/unstable/evaluators/types/evaluator_base.py b/langfuse/api/unstable/evaluators/types/evaluator_base.py new file mode 100644 index 000000000..7a8362657 --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/evaluator_base.py @@ -0,0 +1,64 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from ...commons.types.evaluator_scope import EvaluatorScope + + +class EvaluatorBase(UniversalBaseModel): + id: str = pydantic.Field() + """ + Identifier of this evaluator. + """ + + name: str = pydantic.Field() + """ + Evaluator name. + """ + + version: int = pydantic.Field() + """ + Version number of this evaluator. + """ + + scope: EvaluatorScope = pydantic.Field() + """ + Where this evaluator comes from: your project or Langfuse-managed defaults. + """ + + variables: typing.List[str] = pydantic.Field() + """ + Variables that can be mapped when creating an evaluation rule. + + LLM evaluators require every variable to be mapped exactly once. Code evaluators always expose the fixed runtime payload fields and Langfuse maps them automatically. + """ + + evaluation_rule_count: typing_extensions.Annotated[ + int, FieldMetadata(alias="evaluationRuleCount") + ] = pydantic.Field() + """ + Number of evaluation rules in the project that currently use this evaluator version. + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] = pydantic.Field() + """ + Timestamp when this evaluator was created. + """ + + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] = pydantic.Field() + """ + Timestamp when this evaluator was last updated. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluators/types/evaluators.py b/langfuse/api/unstable/evaluators/types/evaluators.py new file mode 100644 index 000000000..51247a66e --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/evaluators.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from ....utils.pagination.types.meta_response import MetaResponse +from .evaluator import Evaluator + + +class Evaluators(UniversalBaseModel): + data: typing.List[Evaluator] + meta: MetaResponse + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/evaluators/types/llm_as_judge_evaluator.py b/langfuse/api/unstable/evaluators/types/llm_as_judge_evaluator.py new file mode 100644 index 000000000..0cf186f47 --- /dev/null +++ b/langfuse/api/unstable/evaluators/types/llm_as_judge_evaluator.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.serialization import FieldMetadata +from ...commons.types.evaluator_model_config import EvaluatorModelConfig +from ...commons.types.public_evaluator_output_definition import ( + PublicEvaluatorOutputDefinition, +) +from .evaluator_base import EvaluatorBase + + +class LlmAsJudgeEvaluator(EvaluatorBase): + prompt: str = pydantic.Field() + """ + Prompt template used during evaluation. + """ + + output_definition: typing_extensions.Annotated[ + PublicEvaluatorOutputDefinition, FieldMetadata(alias="outputDefinition") + ] = pydantic.Field() + """ + Structured output schema returned by this evaluator. + + Responses always include `dataType` and omit the internal output-definition `version`. + Use `dataType` to decide how future scores should be interpreted. + """ + + model_config_: typing_extensions.Annotated[ + typing.Optional[EvaluatorModelConfig], FieldMetadata(alias="modelConfig") + ] = pydantic.Field(default=None) + """ + Explicit model configuration, or `null` when the project default evaluation model is used. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/raw_client.py b/langfuse/api/unstable/raw_client.py new file mode 100644 index 000000000..5201a5119 --- /dev/null +++ b/langfuse/api/unstable/raw_client.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper + + +class RawUnstableClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + +class AsyncRawUnstableClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper diff --git a/langfuse/api/utils/__init__.py b/langfuse/api/utils/__init__.py new file mode 100644 index 000000000..b272f64b5 --- /dev/null +++ b/langfuse/api/utils/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from . import pagination + from .pagination import MetaResponse +_dynamic_imports: typing.Dict[str, str] = { + "MetaResponse": ".pagination", + "pagination": ".pagination", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["MetaResponse", "pagination"] diff --git a/langfuse/api/utils/pagination/__init__.py b/langfuse/api/utils/pagination/__init__.py new file mode 100644 index 000000000..50821832d --- /dev/null +++ b/langfuse/api/utils/pagination/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import MetaResponse +_dynamic_imports: typing.Dict[str, str] = {"MetaResponse": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["MetaResponse"] diff --git a/langfuse/api/utils/pagination/types/__init__.py b/langfuse/api/utils/pagination/types/__init__.py new file mode 100644 index 000000000..5c0d83028 --- /dev/null +++ b/langfuse/api/utils/pagination/types/__init__.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .meta_response import MetaResponse +_dynamic_imports: typing.Dict[str, str] = {"MetaResponse": ".meta_response"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["MetaResponse"] diff --git a/langfuse/api/utils/pagination/types/meta_response.py b/langfuse/api/utils/pagination/types/meta_response.py new file mode 100644 index 000000000..54d3847be --- /dev/null +++ b/langfuse/api/utils/pagination/types/meta_response.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata + + +class MetaResponse(UniversalBaseModel): + page: int = pydantic.Field() + """ + current page number + """ + + limit: int = pydantic.Field() + """ + number of items per page + """ + + total_items: typing_extensions.Annotated[int, FieldMetadata(alias="totalItems")] = ( + pydantic.Field() + ) + """ + number of total items given the current filters/selection (if any) + """ + + total_pages: typing_extensions.Annotated[int, FieldMetadata(alias="totalPages")] = ( + pydantic.Field() + ) + """ + number of total pages given the current limit + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/batch_evaluation.py b/langfuse/batch_evaluation.py new file mode 100644 index 000000000..d28fd085d --- /dev/null +++ b/langfuse/batch_evaluation.py @@ -0,0 +1,1643 @@ +"""Batch evaluation functionality for Langfuse. + +This module provides comprehensive batch evaluation capabilities for running evaluations +on traces and observations fetched from Langfuse. It includes type definitions, +protocols, result classes, and the implementation for large-scale evaluation workflows +with error handling, retry logic, and resume capability. +""" + +import asyncio +import json +import time +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Dict, + List, + Optional, + Protocol, + Set, + Tuple, + Union, + cast, +) + +from langfuse.api import ( + ObservationsView, + TraceWithFullDetails, +) +from langfuse.experiment import Evaluation, EvaluatorFunction +from langfuse.logger import langfuse_logger as logger + +if TYPE_CHECKING: + from langfuse._client.client import Langfuse + + +class EvaluatorInputs: + """Input data structure for evaluators, returned by mapper functions. + + This class provides a strongly-typed container for transforming API response + objects (traces, observations) into the standardized format expected + by evaluator functions. It ensures consistent access to input, output, expected + output, and metadata regardless of the source entity type. + + Attributes: + input: The input data that was provided to generate the output being evaluated. + For traces, this might be the initial prompt or request. For observations, + this could be the span's input. The exact meaning depends on your use case. + output: The actual output that was produced and needs to be evaluated. + For traces, this is typically the final response. For observations, + this might be the generation output or span result. + expected_output: Optional ground truth or expected result for comparison. + Used by evaluators to assess correctness. May be None if no ground truth + is available for the entity being evaluated. + metadata: Optional structured metadata providing additional context for evaluation. + Can include information about the entity, execution context, user attributes, + or any other relevant data that evaluators might use. + + Examples: + Simple mapper for traces: + ```python + from langfuse import EvaluatorInputs + + def trace_mapper(trace): + return EvaluatorInputs( + input=trace.input, + output=trace.output, + expected_output=None, # No ground truth available + metadata={"user_id": trace.user_id, "tags": trace.tags} + ) + ``` + + Mapper for observations extracting specific fields: + ```python + def observation_mapper(observation): + # Extract input/output from observation's data + input_data = observation.input if hasattr(observation, 'input') else None + output_data = observation.output if hasattr(observation, 'output') else None + + return EvaluatorInputs( + input=input_data, + output=output_data, + expected_output=None, + metadata={ + "observation_type": observation.type, + "model": observation.model, + "latency_ms": observation.end_time - observation.start_time + } + ) + ``` + ``` + + Note: + All arguments must be passed as keywords when instantiating this class. + """ + + def __init__( + self, + *, + input: Any, + output: Any, + expected_output: Any = None, + metadata: Optional[Dict[str, Any]] = None, + ): + """Initialize EvaluatorInputs with the provided data. + + Args: + input: The input data for evaluation. + output: The output data to be evaluated. + expected_output: Optional ground truth for comparison. + metadata: Optional additional context for evaluation. + + Note: + All arguments must be provided as keywords. + """ + self.input = input + self.output = output + self.expected_output = expected_output + self.metadata = metadata + + +class MapperFunction(Protocol): + """Protocol defining the interface for mapper functions in batch evaluation. + + Mapper functions transform API response objects (traces or observations) + into the standardized EvaluatorInputs format that evaluators expect. This abstraction + allows you to define how to extract and structure evaluation data from different + entity types. + + Mapper functions must: + - Accept a single item parameter (trace, observation) + - Return an EvaluatorInputs instance with input, output, expected_output, metadata + - Can be either synchronous or asynchronous + - Should handle missing or malformed data gracefully + """ + + def __call__( + self, + *, + item: Union["TraceWithFullDetails", "ObservationsView"], + **kwargs: Dict[str, Any], + ) -> Union[EvaluatorInputs, Awaitable[EvaluatorInputs]]: + """Transform an API response object into evaluator inputs. + + This method defines how to extract evaluation-relevant data from the raw + API response object. The implementation should map entity-specific fields + to the standardized input/output/expected_output/metadata structure. + + Args: + item: The API response object to transform. The type depends on the scope: + - TraceWithFullDetails: When evaluating traces + - ObservationsView: When evaluating observations + + Returns: + EvaluatorInputs: A structured container with: + - input: The input data that generated the output + - output: The output to be evaluated + - expected_output: Optional ground truth for comparison + - metadata: Optional additional context + + Can return either a direct EvaluatorInputs instance or an awaitable + (for async mappers that need to fetch additional data). + + Examples: + Basic trace mapper: + ```python + def map_trace(trace): + return EvaluatorInputs( + input=trace.input, + output=trace.output, + expected_output=None, + metadata={"trace_id": trace.id, "user": trace.user_id} + ) + ``` + + Observation mapper with conditional logic: + ```python + def map_observation(observation): + # Extract fields based on observation type + if observation.type == "GENERATION": + input_data = observation.input + output_data = observation.output + else: + # For other types, use different fields + input_data = observation.metadata.get("input") + output_data = observation.metadata.get("output") + + return EvaluatorInputs( + input=input_data, + output=output_data, + expected_output=None, + metadata={"obs_id": observation.id, "type": observation.type} + ) + ``` + + Async mapper (if additional processing needed): + ```python + async def map_trace_async(trace): + # Could do async processing here if needed + processed_output = await some_async_transformation(trace.output) + + return EvaluatorInputs( + input=trace.input, + output=processed_output, + expected_output=None, + metadata={"trace_id": trace.id} + ) + ``` + """ + ... + + +class CompositeEvaluatorFunction(Protocol): + """Protocol defining the interface for composite evaluator functions. + + Composite evaluators create aggregate scores from multiple item-level evaluations. + This is commonly used to compute weighted averages, combined metrics, or other + composite assessments based on individual evaluation results. + + Composite evaluators: + - Accept the same inputs as item-level evaluators (input, output, expected_output, metadata) + plus the list of evaluations + - Return either a single Evaluation, a list of Evaluations, or a dict + - Can be either synchronous or asynchronous + - Have access to both raw item data and evaluation results + """ + + def __call__( + self, + *, + input: Optional[Any] = None, + output: Optional[Any] = None, + expected_output: Optional[Any] = None, + metadata: Optional[Dict[str, Any]] = None, + evaluations: List[Evaluation], + **kwargs: Dict[str, Any], + ) -> Union[ + Evaluation, + List[Evaluation], + Dict[str, Any], + Awaitable[Evaluation], + Awaitable[List[Evaluation]], + Awaitable[Dict[str, Any]], + ]: + r"""Create a composite evaluation from item-level evaluation results. + + This method combines multiple evaluation scores into a single composite metric. + Common use cases include weighted averages, pass/fail decisions based on multiple + criteria, or custom scoring logic that considers multiple dimensions. + + Args: + input: The input data that was provided to the system being evaluated. + output: The output generated by the system being evaluated. + expected_output: The expected/reference output for comparison (if available). + metadata: Additional metadata about the evaluation context. + evaluations: List of evaluation results from item-level evaluators. + Each evaluation contains name, value, comment, and metadata. + + Returns: + Can return any of: + - Evaluation: A single composite evaluation result + - List[Evaluation]: Multiple composite evaluations + - Dict: A dict that will be converted to an Evaluation + - name: Identifier for the composite metric (e.g., "composite_score") + - value: The computed composite value + - comment: Optional explanation of how the score was computed + - metadata: Optional details about the composition logic + + Can return either a direct Evaluation instance or an awaitable + (for async composite evaluators). + + Examples: + Simple weighted average: + ```python + def weighted_composite(*, input, output, expected_output, metadata, evaluations): + weights = { + "accuracy": 0.5, + "relevance": 0.3, + "safety": 0.2 + } + + total_score = 0.0 + total_weight = 0.0 + + for eval in evaluations: + if eval.name in weights and isinstance(eval.value, (int, float)): + total_score += eval.value * weights[eval.name] + total_weight += weights[eval.name] + + final_score = total_score / total_weight if total_weight > 0 else 0.0 + + return Evaluation( + name="composite_score", + value=final_score, + comment=f"Weighted average of {len(evaluations)} metrics" + ) + ``` + + Pass/fail composite based on thresholds: + ```python + def pass_fail_composite(*, input, output, expected_output, metadata, evaluations): + # Must pass all criteria + thresholds = { + "accuracy": 0.7, + "safety": 0.9, + "relevance": 0.6 + } + + passes = True + failing_metrics = [] + + for metric, threshold in thresholds.items(): + eval_result = next((e for e in evaluations if e.name == metric), None) + if eval_result and isinstance(eval_result.value, (int, float)): + if eval_result.value < threshold: + passes = False + failing_metrics.append(metric) + + return Evaluation( + name="passes_all_checks", + value=passes, + comment=f"Failed: {', '.join(failing_metrics)}" if failing_metrics else "All checks passed", + data_type="BOOLEAN" + ) + ``` + + Async composite with external scoring: + ```python + async def llm_composite(*, input, output, expected_output, metadata, evaluations): + # Use LLM to synthesize multiple evaluation results + eval_summary = "\n".join( + f"- {e.name}: {e.value}" for e in evaluations + ) + + prompt = f"Given these evaluation scores:\n{eval_summary}\n" + prompt += f"For the output: {output}\n" + prompt += "Provide an overall quality score from 0-1." + + response = await openai.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": prompt}] + ) + + score = float(response.choices[0].message.content.strip()) + + return Evaluation( + name="llm_composite_score", + value=score, + comment="LLM-synthesized composite score" + ) + ``` + + Context-aware composite: + ```python + def context_composite(*, input, output, expected_output, metadata, evaluations): + # Adjust weighting based on metadata + base_weights = {"accuracy": 0.5, "speed": 0.3, "cost": 0.2} + + # If metadata indicates high importance, prioritize accuracy + if metadata and metadata.get('importance') == 'high': + weights = {"accuracy": 0.7, "speed": 0.2, "cost": 0.1} + else: + weights = base_weights + + total = sum( + e.value * weights.get(e.name, 0) + for e in evaluations + if isinstance(e.value, (int, float)) + ) + + return Evaluation( + name="weighted_composite", + value=total, + comment="Context-aware weighted composite" + ) + ``` + """ + ... + + +class EvaluatorStats: + """Statistics for a single evaluator's performance during batch evaluation. + + This class tracks detailed metrics about how a specific evaluator performed + across all items in a batch evaluation run. It helps identify evaluator issues, + understand reliability, and optimize evaluation pipelines. + + Attributes: + name: The name of the evaluator function (extracted from __name__). + total_runs: Total number of times the evaluator was invoked. + successful_runs: Number of times the evaluator completed successfully. + failed_runs: Number of times the evaluator raised an exception or failed. + total_scores_created: Total number of evaluation scores created by this evaluator. + Can be higher than successful_runs if the evaluator returns multiple scores. + + Examples: + Accessing evaluator stats from batch evaluation result: + ```python + result = client.run_batched_evaluation(...) + + for stats in result.evaluator_stats: + print(f"Evaluator: {stats.name}") + print(f" Success rate: {stats.successful_runs / stats.total_runs:.1%}") + print(f" Scores created: {stats.total_scores_created}") + + if stats.failed_runs > 0: + print(f" ⚠️ Failed {stats.failed_runs} times") + ``` + + Identifying problematic evaluators: + ```python + result = client.run_batched_evaluation(...) + + # Find evaluators with high failure rates + for stats in result.evaluator_stats: + failure_rate = stats.failed_runs / stats.total_runs + if failure_rate > 0.1: # More than 10% failures + print(f"⚠️ {stats.name} has {failure_rate:.1%} failure rate") + print(f" Consider debugging or removing this evaluator") + ``` + + Note: + All arguments must be passed as keywords when instantiating this class. + """ + + def __init__( + self, + *, + name: str, + total_runs: int = 0, + successful_runs: int = 0, + failed_runs: int = 0, + total_scores_created: int = 0, + ): + """Initialize EvaluatorStats with the provided metrics. + + Args: + name: The evaluator function name. + total_runs: Total number of evaluator invocations. + successful_runs: Number of successful completions. + failed_runs: Number of failures. + total_scores_created: Total scores created by this evaluator. + + Note: + All arguments must be provided as keywords. + """ + self.name = name + self.total_runs = total_runs + self.successful_runs = successful_runs + self.failed_runs = failed_runs + self.total_scores_created = total_scores_created + + +class BatchEvaluationResumeToken: + """Token for resuming a failed batch evaluation run. + + This class encapsulates all the information needed to resume a batch evaluation + that was interrupted or failed partway through. It uses timestamp-based filtering + to avoid re-processing items that were already evaluated, even if the underlying + dataset changed between runs. + + Attributes: + scope: The type of items being evaluated ("traces", "observations"). + filter: The original JSON filter string used to query items. + last_processed_timestamp: ISO 8601 timestamp of the last successfully processed item. + Used to construct a filter that only fetches items after this timestamp. + last_processed_id: The ID of the last successfully processed item, for reference. + items_processed: Count of items successfully processed before interruption. + + Examples: + Resuming a failed batch evaluation: + ```python + # Initial run that fails partway through + try: + result = client.run_batched_evaluation( + scope="traces", + mapper=my_mapper, + evaluators=[evaluator1, evaluator2], + filter='{"tags": ["production"]}', + max_items=10000 + ) + except Exception as e: + print(f"Evaluation failed: {e}") + + # Save the resume token + if result.resume_token: + # Store resume token for later (e.g., in a file or database) + import json + with open("resume_token.json", "w") as f: + json.dump({ + "scope": result.resume_token.scope, + "filter": result.resume_token.filter, + "last_timestamp": result.resume_token.last_processed_timestamp, + "last_id": result.resume_token.last_processed_id, + "items_done": result.resume_token.items_processed + }, f) + + # Later, resume from where it left off + with open("resume_token.json") as f: + token_data = json.load(f) + + resume_token = BatchEvaluationResumeToken( + scope=token_data["scope"], + filter=token_data["filter"], + last_processed_timestamp=token_data["last_timestamp"], + last_processed_id=token_data["last_id"], + items_processed=token_data["items_done"] + ) + + # Resume the evaluation + result = client.run_batched_evaluation( + scope="traces", + mapper=my_mapper, + evaluators=[evaluator1, evaluator2], + resume_from=resume_token + ) + + print(f"Processed {result.total_items_processed} additional items") + ``` + + Handling partial completion: + ```python + result = client.run_batched_evaluation(...) + + if not result.completed: + print(f"Evaluation incomplete. Processed {result.resume_token.items_processed} items") + print(f"Last item: {result.resume_token.last_processed_id}") + print(f"Resume from: {result.resume_token.last_processed_timestamp}") + + # Optionally retry automatically + if result.resume_token: + print("Retrying...") + result = client.run_batched_evaluation( + scope=result.resume_token.scope, + mapper=my_mapper, + evaluators=my_evaluators, + resume_from=result.resume_token + ) + ``` + + Note: + All arguments must be passed as keywords when instantiating this class. + The timestamp-based approach means that items created after the initial run + but before the timestamp will be skipped. This is intentional to avoid + duplicates and ensure consistent evaluation. + """ + + def __init__( + self, + *, + scope: str, + filter: Optional[str], + last_processed_timestamp: str, + last_processed_id: str, + items_processed: int, + ): + """Initialize BatchEvaluationResumeToken with the provided state. + + Args: + scope: The scope type ("traces", "observations"). + filter: The original JSON filter string. + last_processed_timestamp: ISO 8601 timestamp of last processed item. + last_processed_id: ID of last processed item. + items_processed: Count of items processed before interruption. + + Note: + All arguments must be provided as keywords. + """ + self.scope = scope + self.filter = filter + self.last_processed_timestamp = last_processed_timestamp + self.last_processed_id = last_processed_id + self.items_processed = items_processed + + +class BatchEvaluationResult: + r"""Complete result structure for batch evaluation execution. + + This class encapsulates comprehensive statistics and metadata about a batch + evaluation run, including counts, evaluator-specific metrics, timing information, + error details, and resume capability. + + Attributes: + total_items_fetched: Total number of items fetched from the API. + total_items_processed: Number of items successfully evaluated. + total_items_failed: Number of items that failed during evaluation. + total_scores_created: Total scores created by all item-level evaluators. + total_composite_scores_created: Scores created by the composite evaluator. + total_evaluations_failed: Number of individual evaluator failures across all items. + evaluator_stats: List of per-evaluator statistics (success/failure rates, scores created). + resume_token: Token for resuming if evaluation was interrupted (None if completed). + completed: True if all items were processed, False if stopped early or failed. + duration_seconds: Total time taken to execute the batch evaluation. + failed_item_ids: List of IDs for items that failed evaluation. + error_summary: Dictionary mapping error types to occurrence counts. + has_more_items: True if max_items limit was reached but more items exist. + item_evaluations: Dictionary mapping item IDs to their evaluation results (both regular and composite). + + Examples: + Basic result inspection: + ```python + result = client.run_batched_evaluation(...) + + print(f"Processed: {result.total_items_processed}/{result.total_items_fetched}") + print(f"Scores created: {result.total_scores_created}") + print(f"Duration: {result.duration_seconds:.2f}s") + print(f"Success rate: {result.total_items_processed / result.total_items_fetched:.1%}") + ``` + + Detailed analysis with evaluator stats: + ```python + result = client.run_batched_evaluation(...) + + print(f"\n📊 Batch Evaluation Results") + print(f"{'='*50}") + print(f"Items processed: {result.total_items_processed}") + print(f"Items failed: {result.total_items_failed}") + print(f"Scores created: {result.total_scores_created}") + + if result.total_composite_scores_created > 0: + print(f"Composite scores: {result.total_composite_scores_created}") + + print(f"\n📈 Evaluator Performance:") + for stats in result.evaluator_stats: + success_rate = stats.successful_runs / stats.total_runs if stats.total_runs > 0 else 0 + print(f"\n {stats.name}:") + print(f" Success rate: {success_rate:.1%}") + print(f" Scores created: {stats.total_scores_created}") + if stats.failed_runs > 0: + print(f" ⚠️ Failures: {stats.failed_runs}") + + if result.error_summary: + print(f"\n⚠️ Errors encountered:") + for error_type, count in result.error_summary.items(): + print(f" {error_type}: {count}") + ``` + + Handling incomplete runs: + ```python + result = client.run_batched_evaluation(...) + + if not result.completed: + print("⚠️ Evaluation incomplete!") + + if result.resume_token: + print(f"Processed {result.resume_token.items_processed} items before failure") + print(f"Use resume_from parameter to continue from:") + print(f" Timestamp: {result.resume_token.last_processed_timestamp}") + print(f" Last ID: {result.resume_token.last_processed_id}") + + if result.has_more_items: + print(f"ℹ️ More items available beyond max_items limit") + ``` + + Performance monitoring: + ```python + result = client.run_batched_evaluation(...) + + items_per_second = result.total_items_processed / result.duration_seconds + avg_scores_per_item = result.total_scores_created / result.total_items_processed + + print(f"Performance metrics:") + print(f" Throughput: {items_per_second:.2f} items/second") + print(f" Avg scores/item: {avg_scores_per_item:.2f}") + print(f" Total duration: {result.duration_seconds:.2f}s") + + if result.total_evaluations_failed > 0: + failure_rate = result.total_evaluations_failed / ( + result.total_items_processed * len(result.evaluator_stats) + ) + print(f" Evaluation failure rate: {failure_rate:.1%}") + ``` + + Note: + All arguments must be passed as keywords when instantiating this class. + """ + + def __init__( + self, + *, + total_items_fetched: int, + total_items_processed: int, + total_items_failed: int, + total_scores_created: int, + total_composite_scores_created: int, + total_evaluations_failed: int, + evaluator_stats: List[EvaluatorStats], + resume_token: Optional[BatchEvaluationResumeToken], + completed: bool, + duration_seconds: float, + failed_item_ids: List[str], + error_summary: Dict[str, int], + has_more_items: bool, + item_evaluations: Dict[str, List["Evaluation"]], + ): + """Initialize BatchEvaluationResult with comprehensive statistics. + + Args: + total_items_fetched: Total items fetched from API. + total_items_processed: Items successfully evaluated. + total_items_failed: Items that failed evaluation. + total_scores_created: Scores from item-level evaluators. + total_composite_scores_created: Scores from composite evaluator. + total_evaluations_failed: Individual evaluator failures. + evaluator_stats: Per-evaluator statistics. + resume_token: Token for resuming (None if completed). + completed: Whether all items were processed. + duration_seconds: Total execution time. + failed_item_ids: IDs of failed items. + error_summary: Error types and counts. + has_more_items: Whether more items exist beyond max_items. + item_evaluations: Dictionary mapping item IDs to their evaluation results. + + Note: + All arguments must be provided as keywords. + """ + self.total_items_fetched = total_items_fetched + self.total_items_processed = total_items_processed + self.total_items_failed = total_items_failed + self.total_scores_created = total_scores_created + self.total_composite_scores_created = total_composite_scores_created + self.total_evaluations_failed = total_evaluations_failed + self.evaluator_stats = evaluator_stats + self.resume_token = resume_token + self.completed = completed + self.duration_seconds = duration_seconds + self.failed_item_ids = failed_item_ids + self.error_summary = error_summary + self.has_more_items = has_more_items + self.item_evaluations = item_evaluations + + def __str__(self) -> str: + """Return a formatted string representation of the batch evaluation results. + + Returns: + A multi-line string with a summary of the evaluation results. + """ + lines = [] + lines.append("=" * 60) + lines.append("Batch Evaluation Results") + lines.append("=" * 60) + + # Summary statistics + lines.append(f"\nStatus: {'Completed' if self.completed else 'Incomplete'}") + lines.append(f"Duration: {self.duration_seconds:.2f}s") + lines.append(f"\nItems fetched: {self.total_items_fetched}") + lines.append(f"Items processed: {self.total_items_processed}") + + if self.total_items_failed > 0: + lines.append(f"Items failed: {self.total_items_failed}") + + # Success rate + if self.total_items_fetched > 0: + success_rate = self.total_items_processed / self.total_items_fetched * 100 + lines.append(f"Success rate: {success_rate:.1f}%") + + # Scores created + lines.append(f"\nScores created: {self.total_scores_created}") + if self.total_composite_scores_created > 0: + lines.append(f"Composite scores: {self.total_composite_scores_created}") + + total_scores = self.total_scores_created + self.total_composite_scores_created + lines.append(f"Total scores: {total_scores}") + + # Evaluator statistics + if self.evaluator_stats: + lines.append("\nEvaluator Performance:") + for stats in self.evaluator_stats: + lines.append(f" {stats.name}:") + if stats.total_runs > 0: + success_rate = ( + stats.successful_runs / stats.total_runs * 100 + if stats.total_runs > 0 + else 0 + ) + lines.append( + f" Runs: {stats.successful_runs}/{stats.total_runs} " + f"({success_rate:.1f}% success)" + ) + lines.append(f" Scores created: {stats.total_scores_created}") + if stats.failed_runs > 0: + lines.append(f" Failed runs: {stats.failed_runs}") + + # Performance metrics + if self.total_items_processed > 0 and self.duration_seconds > 0: + items_per_sec = self.total_items_processed / self.duration_seconds + lines.append("\nPerformance:") + lines.append(f" Throughput: {items_per_sec:.2f} items/second") + if self.total_scores_created > 0: + avg_scores = self.total_scores_created / self.total_items_processed + lines.append(f" Avg scores per item: {avg_scores:.2f}") + + # Errors and warnings + if self.error_summary: + lines.append("\nErrors encountered:") + for error_type, count in self.error_summary.items(): + lines.append(f" {error_type}: {count}") + + # Incomplete run information + if not self.completed: + lines.append("\nWarning: Evaluation incomplete") + if self.resume_token: + lines.append( + f" Last processed: {self.resume_token.last_processed_timestamp}" + ) + lines.append(f" Items processed: {self.resume_token.items_processed}") + lines.append(" Use resume_from parameter to continue") + + if self.has_more_items: + lines.append("\nNote: More items available beyond max_items limit") + + lines.append("=" * 60) + return "\n".join(lines) + + +class BatchEvaluationRunner: + """Handles batch evaluation execution for a Langfuse client. + + This class encapsulates all the logic for fetching items, running evaluators, + creating scores, and managing the evaluation lifecycle. It provides a clean + separation of concerns from the main Langfuse client class. + + The runner uses a streaming/pipeline approach to process items in batches, + avoiding loading the entire dataset into memory. This makes it suitable for + evaluating large numbers of items. + + Attributes: + client: The Langfuse client instance used for API calls and score creation. + """ + + def __init__(self, client: "Langfuse"): + """Initialize the batch evaluation runner. + + Args: + client: The Langfuse client instance. + """ + self.client = client + + async def run_async( + self, + *, + scope: str, + mapper: MapperFunction, + evaluators: List[EvaluatorFunction], + filter: Optional[str] = None, + fetch_batch_size: int = 50, + fetch_trace_fields: Optional[str] = "io", + max_items: Optional[int] = None, + max_concurrency: int = 5, + composite_evaluator: Optional[CompositeEvaluatorFunction] = None, + metadata: Optional[Dict[str, Any]] = None, + _add_observation_scores_to_trace: bool = False, + _additional_trace_tags: Optional[List[str]] = None, + max_retries: int = 3, + verbose: bool = False, + resume_from: Optional[BatchEvaluationResumeToken] = None, + ) -> BatchEvaluationResult: + """Run batch evaluation asynchronously. + + This is the main implementation method that orchestrates the entire batch + evaluation process: fetching items, mapping, evaluating, creating scores, + and tracking statistics. + + Args: + scope: The type of items to evaluate ("traces", "observations"). + mapper: Function to transform API response items to evaluator inputs. + evaluators: List of evaluation functions to run on each item. + filter: JSON filter string for querying items. + fetch_batch_size: Number of items to fetch per API call. + fetch_trace_fields: Comma-separated list of fields to include when fetching traces. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. Only relevant if scope is 'traces'. Default: 'io' + max_items: Maximum number of items to process (None = all). + max_concurrency: Maximum number of concurrent evaluations. + composite_evaluator: Optional function to create composite scores. + metadata: Metadata to add to all created scores. + _add_observation_scores_to_trace: Private option to duplicate + observation-level scores onto the parent trace. + _additional_trace_tags: Private option to add tags on traces via + ingestion trace-create events. + max_retries: Maximum retries for failed batch fetches. + verbose: If True, log progress to console. + resume_from: Resume token from a previous failed run. + + Returns: + BatchEvaluationResult with comprehensive statistics. + """ + start_time = time.time() + + # Initialize tracking variables + total_items_fetched = 0 + total_items_processed = 0 + total_items_failed = 0 + total_scores_created = 0 + total_composite_scores_created = 0 + total_evaluations_failed = 0 + failed_item_ids: List[str] = [] + error_summary: Dict[str, int] = {} + item_evaluations: Dict[str, List[Evaluation]] = {} + + # Initialize evaluator stats + evaluator_stats_dict = { + getattr(evaluator, "__name__", "unknown_evaluator"): EvaluatorStats( + name=getattr(evaluator, "__name__", "unknown_evaluator") + ) + for evaluator in evaluators + } + + # Handle resume token by modifying filter + effective_filter = self._build_timestamp_filter(filter, resume_from) + normalized_additional_trace_tags = ( + self._dedupe_tags(_additional_trace_tags) + if _additional_trace_tags is not None + else [] + ) + updated_trace_ids: Set[str] = set() + + # Create semaphore for concurrency control + semaphore = asyncio.Semaphore(max_concurrency) + + # Pagination state + page = 1 + has_more = True + last_item_timestamp: Optional[str] = None + last_item_id: Optional[str] = None + + if verbose: + logger.info(f"Starting batch evaluation on {scope}") + if scope == "traces" and fetch_trace_fields: + logger.info(f"Fetching trace fields: {fetch_trace_fields}") + if resume_from: + logger.info( + f"Resuming from {resume_from.last_processed_timestamp} " + f"({resume_from.items_processed} items already processed)" + ) + + # Main pagination loop + while has_more: + # Check if we've reached max_items + if max_items is not None and total_items_fetched >= max_items: + if verbose: + logger.info(f"Reached max_items limit ({max_items})") + has_more = True # More items may exist + break + + # Fetch next batch with retry logic + try: + items = await self._fetch_batch_with_retry( + scope=scope, + filter=effective_filter, + page=page, + limit=fetch_batch_size, + max_retries=max_retries, + fields=fetch_trace_fields, + ) + except Exception as e: + # Failed after max_retries - create resume token and return + error_msg = f"Failed to fetch batch after {max_retries} retries" + logger.error(f"{error_msg}: {e}") + + resume_token = BatchEvaluationResumeToken( + scope=scope, + filter=filter, # Original filter, not modified + last_processed_timestamp=last_item_timestamp or "", + last_processed_id=last_item_id or "", + items_processed=total_items_processed, + ) + + return self._build_result( + total_items_fetched=total_items_fetched, + total_items_processed=total_items_processed, + total_items_failed=total_items_failed, + total_scores_created=total_scores_created, + total_composite_scores_created=total_composite_scores_created, + total_evaluations_failed=total_evaluations_failed, + evaluator_stats_dict=evaluator_stats_dict, + resume_token=resume_token, + completed=False, + start_time=start_time, + failed_item_ids=failed_item_ids, + error_summary=error_summary, + has_more_items=has_more, + item_evaluations=item_evaluations, + ) + + # Check if we got any items + if not items: + has_more = False + if verbose: + logger.info("No more items to fetch") + break + + total_items_fetched += len(items) + + if verbose: + logger.info(f"Fetched batch {page} ({len(items)} items)") + + # Limit items if max_items would be exceeded + items_to_process = items + if max_items is not None: + remaining_capacity = max_items - total_items_processed + if len(items) > remaining_capacity: + items_to_process = items[:remaining_capacity] + if verbose: + logger.info( + f"Limiting batch to {len(items_to_process)} items " + f"to respect max_items={max_items}" + ) + + # Process items concurrently + async def process_item( + item: Union[TraceWithFullDetails, ObservationsView], + ) -> Tuple[str, Union[Tuple[int, int, int, List[Evaluation]], Exception]]: + """Process a single item and return (item_id, result).""" + async with semaphore: + item_id = self._get_item_id(item, scope) + try: + result = await self._process_batch_evaluation_item( + item=item, + scope=scope, + mapper=mapper, + evaluators=evaluators, + composite_evaluator=composite_evaluator, + metadata=metadata, + _add_observation_scores_to_trace=_add_observation_scores_to_trace, + evaluator_stats_dict=evaluator_stats_dict, + ) + return (item_id, result) + except Exception as e: + return (item_id, e) + + # Run all items in batch concurrently + tasks = [process_item(item) for item in items_to_process] + results = await asyncio.gather(*tasks) + + # Process results and update statistics + for item, (item_id, result) in zip(items_to_process, results): + if isinstance(result, Exception): + # Item processing failed + total_items_failed += 1 + failed_item_ids.append(item_id) + error_type = type(result).__name__ + error_summary[error_type] = error_summary.get(error_type, 0) + 1 + logger.warning(f"Item {item_id} failed: {result}") + else: + # Item processed successfully + total_items_processed += 1 + scores_created, composite_created, evals_failed, evaluations = ( + result + ) + total_scores_created += scores_created + total_composite_scores_created += composite_created + total_evaluations_failed += evals_failed + + # Store evaluations for this item + item_evaluations[item_id] = evaluations + + if normalized_additional_trace_tags: + trace_id = ( + item_id + if scope == "traces" + else cast(ObservationsView, item).trace_id + ) + + if trace_id and trace_id not in updated_trace_ids: + self.client._create_trace_tags_via_ingestion( + trace_id=trace_id, + tags=normalized_additional_trace_tags, + ) + updated_trace_ids.add(trace_id) + + # Update last processed tracking + last_item_timestamp = self._get_item_timestamp(item, scope) + last_item_id = item_id + + if verbose: + if max_items is not None and max_items > 0: + progress_pct = total_items_processed / max_items * 100 + logger.info( + f"Progress: {total_items_processed}/{max_items} items " + f"({progress_pct:.1f}%), {total_scores_created} scores created" + ) + else: + logger.info( + f"Progress: {total_items_processed} items processed, " + f"{total_scores_created} scores created" + ) + + # Check if we should continue to next page + if len(items) < fetch_batch_size: + # Last page - no more items available + has_more = False + else: + page += 1 + + # Check max_items again before next fetch + if max_items is not None and total_items_fetched >= max_items: + has_more = True # More items exist but we're stopping + break + + # Flush all scores to Langfuse + if verbose: + logger.info("Flushing scores to Langfuse...") + self.client.flush() + + # Build final result + duration = time.time() - start_time + + if verbose: + logger.info( + f"Batch evaluation complete: {total_items_processed} items processed " + f"in {duration:.2f}s" + ) + + # Completed successfully if we either: + # 1. Ran out of items (has_more is False), OR + # 2. Hit max_items limit (intentionally stopped) + completed_successfully = not has_more or ( + max_items is not None and total_items_fetched >= max_items + ) + + return self._build_result( + total_items_fetched=total_items_fetched, + total_items_processed=total_items_processed, + total_items_failed=total_items_failed, + total_scores_created=total_scores_created, + total_composite_scores_created=total_composite_scores_created, + total_evaluations_failed=total_evaluations_failed, + evaluator_stats_dict=evaluator_stats_dict, + resume_token=None, # No resume needed on successful completion + completed=completed_successfully, + start_time=start_time, + failed_item_ids=failed_item_ids, + error_summary=error_summary, + has_more_items=( + has_more and max_items is not None and total_items_fetched >= max_items + ), + item_evaluations=item_evaluations, + ) + + async def _fetch_batch_with_retry( + self, + *, + scope: str, + filter: Optional[str], + page: int, + limit: int, + max_retries: int, + fields: Optional[str], + ) -> List[Union[TraceWithFullDetails, ObservationsView]]: + """Fetch a batch of items with retry logic. + + Args: + scope: The type of items ("traces", "observations"). + filter: JSON filter string for querying. + page: Page number (1-indexed). + limit: Number of items per page. + max_retries: Maximum number of retry attempts. + verbose: Whether to log retry attempts. + fields: Trace fields to fetch + + Returns: + List of items from the API. + + Raises: + Exception: If all retry attempts fail. + """ + if scope == "traces": + response = self.client.api.trace.list( + page=page, + limit=limit, + filter=filter, + request_options={"max_retries": max_retries}, + fields=fields, + ) # type: ignore + return list(response.data) # type: ignore + elif scope == "observations": + response = self.client.api.legacy.observations_v1.get_many( + page=page, + limit=limit, + filter=filter, + request_options={"max_retries": max_retries}, + ) # type: ignore + return list(response.data) # type: ignore + else: + error_message = f"Invalid scope: {scope}" + raise ValueError(error_message) + + async def _process_batch_evaluation_item( + self, + item: Union[TraceWithFullDetails, ObservationsView], + scope: str, + mapper: MapperFunction, + evaluators: List[EvaluatorFunction], + composite_evaluator: Optional[CompositeEvaluatorFunction], + metadata: Optional[Dict[str, Any]], + _add_observation_scores_to_trace: bool, + evaluator_stats_dict: Dict[str, EvaluatorStats], + ) -> Tuple[int, int, int, List[Evaluation]]: + """Process a single item: map, evaluate, create scores. + + Args: + item: The API response object to evaluate. + scope: The type of item ("traces", "observations"). + mapper: Function to transform item to evaluator inputs. + evaluators: List of evaluator functions. + composite_evaluator: Optional composite evaluator function. + metadata: Additional metadata to add to scores. + _add_observation_scores_to_trace: Whether to duplicate + observation-level scores at trace level. + evaluator_stats_dict: Dictionary tracking evaluator statistics. + + Returns: + Tuple of (scores_created, composite_scores_created, evaluations_failed, all_evaluations). + + Raises: + Exception: If mapping fails or item processing encounters fatal error. + """ + scores_created = 0 + composite_scores_created = 0 + evaluations_failed = 0 + + # Run mapper to transform item + evaluator_inputs = await self._run_mapper(mapper, item) + + # Run all evaluators + evaluations: List[Evaluation] = [] + for evaluator in evaluators: + evaluator_name = getattr(evaluator, "__name__", "unknown_evaluator") + stats = evaluator_stats_dict[evaluator_name] + stats.total_runs += 1 + + try: + eval_results = await self._run_evaluator_internal( + evaluator, + input=evaluator_inputs.input, + output=evaluator_inputs.output, + expected_output=evaluator_inputs.expected_output, + metadata=evaluator_inputs.metadata, + ) + + stats.successful_runs += 1 + stats.total_scores_created += len(eval_results) + evaluations.extend(eval_results) + + except Exception as e: + # Evaluator failed - log warning and continue with other evaluators + stats.failed_runs += 1 + evaluations_failed += 1 + logger.warning( + f"Evaluator {evaluator_name} failed on item " + f"{self._get_item_id(item, scope)}: {e}" + ) + + # Create scores for item-level evaluations + item_id = self._get_item_id(item, scope) + for evaluation in evaluations: + scores_created += self._create_score_for_scope( + scope=scope, + item_id=item_id, + trace_id=cast(ObservationsView, item).trace_id + if scope == "observations" + else None, + evaluation=evaluation, + additional_metadata=metadata, + add_observation_score_to_trace=_add_observation_scores_to_trace, + ) + + # Run composite evaluator if provided and we have evaluations + if composite_evaluator and evaluations: + try: + composite_evals = await self._run_composite_evaluator( + composite_evaluator, + input=evaluator_inputs.input, + output=evaluator_inputs.output, + expected_output=evaluator_inputs.expected_output, + metadata=evaluator_inputs.metadata, + evaluations=evaluations, + ) + + # Create scores for all composite evaluations + for composite_eval in composite_evals: + composite_scores_created += self._create_score_for_scope( + scope=scope, + item_id=item_id, + trace_id=cast(ObservationsView, item).trace_id + if scope == "observations" + else None, + evaluation=composite_eval, + additional_metadata=metadata, + add_observation_score_to_trace=_add_observation_scores_to_trace, + ) + + # Add composite evaluations to the list + evaluations.extend(composite_evals) + + except Exception as e: + logger.warning(f"Composite evaluator failed on item {item_id}: {e}") + + return ( + scores_created, + composite_scores_created, + evaluations_failed, + evaluations, + ) + + async def _run_evaluator_internal( + self, + evaluator: EvaluatorFunction, + **kwargs: Any, + ) -> List[Evaluation]: + """Run an evaluator function and normalize the result. + + Unlike experiment._run_evaluator, this version raises exceptions + so we can track failures in our statistics. + + Args: + evaluator: The evaluator function to run. + **kwargs: Arguments to pass to the evaluator. + + Returns: + List of Evaluation objects. + + Raises: + Exception: If evaluator raises an exception (not caught). + """ + result = evaluator(**kwargs) + + # Handle async evaluators + if asyncio.iscoroutine(result): + result = await result + + # Normalize to list + if isinstance(result, (dict, Evaluation)): + return [result] # type: ignore + elif isinstance(result, list): + return result + else: + return [] + + async def _run_mapper( + self, + mapper: MapperFunction, + item: Union[TraceWithFullDetails, ObservationsView], + ) -> EvaluatorInputs: + """Run mapper function (handles both sync and async mappers). + + Args: + mapper: The mapper function to run. + item: The API response object to map. + + Returns: + EvaluatorInputs instance. + + Raises: + Exception: If mapper raises an exception. + """ + result = mapper(item=item) + if asyncio.iscoroutine(result): + return await result # type: ignore + return result # type: ignore + + async def _run_composite_evaluator( + self, + composite_evaluator: CompositeEvaluatorFunction, + input: Optional[Any], + output: Optional[Any], + expected_output: Optional[Any], + metadata: Optional[Dict[str, Any]], + evaluations: List[Evaluation], + ) -> List[Evaluation]: + """Run composite evaluator function (handles both sync and async). + + Args: + composite_evaluator: The composite evaluator function. + input: The input data provided to the system. + output: The output generated by the system. + expected_output: The expected/reference output. + metadata: Additional metadata about the evaluation context. + evaluations: List of item-level evaluations. + + Returns: + List of Evaluation objects (normalized from single or list return). + + Raises: + Exception: If composite evaluator raises an exception. + """ + result = composite_evaluator( + input=input, + output=output, + expected_output=expected_output, + metadata=metadata, + evaluations=evaluations, + ) + if asyncio.iscoroutine(result): + result = await result + + # Normalize to list (same as regular evaluator) + if isinstance(result, (dict, Evaluation)): + return [result] # type: ignore + elif isinstance(result, list): + return result + else: + return [] + + def _create_score_for_scope( + self, + *, + scope: str, + item_id: str, + trace_id: Optional[str] = None, + evaluation: Evaluation, + additional_metadata: Optional[Dict[str, Any]], + add_observation_score_to_trace: bool = False, + ) -> int: + """Create a score linked to the appropriate entity based on scope. + + Args: + scope: The type of entity ("traces", "observations"). + item_id: The ID of the entity. + trace_id: The trace ID of the entity; required if scope=observations + evaluation: The evaluation result to create a score from. + additional_metadata: Additional metadata to merge with evaluation metadata. + add_observation_score_to_trace: Whether to duplicate observation + score on parent trace as well. + + Returns: + Number of score events created. + """ + # Merge metadata + score_metadata = { + **(evaluation.metadata or {}), + **(additional_metadata or {}), + } + + if scope == "traces": + self.client.create_score( + trace_id=item_id, + name=evaluation.name, + value=evaluation.value, # type: ignore + comment=evaluation.comment, + metadata=score_metadata, + data_type=evaluation.data_type, # type: ignore[arg-type] + config_id=evaluation.config_id, + ) + return 1 + elif scope == "observations": + self.client.create_score( + observation_id=item_id, + trace_id=trace_id, + name=evaluation.name, + value=evaluation.value, # type: ignore + comment=evaluation.comment, + metadata=score_metadata, + data_type=evaluation.data_type, # type: ignore[arg-type] + config_id=evaluation.config_id, + ) + score_count = 1 + + if add_observation_score_to_trace and trace_id: + self.client.create_score( + trace_id=trace_id, + name=evaluation.name, + value=evaluation.value, # type: ignore + comment=evaluation.comment, + metadata=score_metadata, + data_type=evaluation.data_type, # type: ignore[arg-type] + config_id=evaluation.config_id, + ) + score_count += 1 + + return score_count + + return 0 + + def _build_timestamp_filter( + self, + original_filter: Optional[str], + resume_from: Optional[BatchEvaluationResumeToken], + ) -> Optional[str]: + """Build filter with timestamp constraint for resume capability. + + Args: + original_filter: The original JSON filter string. + resume_from: Optional resume token with timestamp information. + + Returns: + Modified filter string with timestamp constraint, or original filter. + """ + if not resume_from: + return original_filter + + # Parse original filter (should be array) or create empty array + try: + filter_list = json.loads(original_filter) if original_filter else [] + if not isinstance(filter_list, list): + logger.warning( + f"Filter should be a JSON array, got: {type(filter_list).__name__}" + ) + filter_list = [] + except json.JSONDecodeError: + logger.warning( + f"Invalid JSON in original filter, ignoring: {original_filter}" + ) + filter_list = [] + + # Add timestamp constraint to filter array + timestamp_field = self._get_timestamp_field_for_scope(resume_from.scope) + timestamp_filter = { + "type": "datetime", + "column": timestamp_field, + "operator": ">", + "value": resume_from.last_processed_timestamp, + } + filter_list.append(timestamp_filter) + + return json.dumps(filter_list) + + @staticmethod + def _get_item_id( + item: Union[TraceWithFullDetails, ObservationsView], + scope: str, + ) -> str: + """Extract ID from item based on scope. + + Args: + item: The API response object. + scope: The type of item. + + Returns: + The item's ID. + """ + return item.id + + @staticmethod + def _get_item_timestamp( + item: Union[TraceWithFullDetails, ObservationsView], + scope: str, + ) -> str: + """Extract timestamp from item based on scope. + + Args: + item: The API response object. + scope: The type of item. + + Returns: + ISO 8601 timestamp string. + """ + if scope == "traces": + # Type narrowing for traces + if hasattr(item, "timestamp"): + return item.timestamp.isoformat() # type: ignore[attr-defined] + elif scope == "observations": + # Type narrowing for observations + if hasattr(item, "start_time"): + return item.start_time.isoformat() # type: ignore[attr-defined] + return "" + + @staticmethod + def _get_timestamp_field_for_scope(scope: str) -> str: + """Get the timestamp field name for filtering based on scope. + + Args: + scope: The type of items. + + Returns: + The field name to use in filters. + """ + if scope == "traces": + return "timestamp" + elif scope == "observations": + return "start_time" + return "timestamp" # Default + + @staticmethod + def _dedupe_tags(tags: Optional[List[str]]) -> List[str]: + """Deduplicate tags while preserving order.""" + if tags is None: + return [] + + deduped: List[str] = [] + seen = set() + for tag in tags: + if tag not in seen: + deduped.append(tag) + seen.add(tag) + + return deduped + + def _build_result( + self, + total_items_fetched: int, + total_items_processed: int, + total_items_failed: int, + total_scores_created: int, + total_composite_scores_created: int, + total_evaluations_failed: int, + evaluator_stats_dict: Dict[str, EvaluatorStats], + resume_token: Optional[BatchEvaluationResumeToken], + completed: bool, + start_time: float, + failed_item_ids: List[str], + error_summary: Dict[str, int], + has_more_items: bool, + item_evaluations: Dict[str, List[Evaluation]], + ) -> BatchEvaluationResult: + """Build the final BatchEvaluationResult. + + Args: + total_items_fetched: Total items fetched. + total_items_processed: Items successfully processed. + total_items_failed: Items that failed. + total_scores_created: Scores from item evaluators. + total_composite_scores_created: Scores from composite evaluator. + total_evaluations_failed: Individual evaluator failures. + evaluator_stats_dict: Per-evaluator statistics. + resume_token: Resume token if incomplete. + completed: Whether evaluation completed fully. + start_time: Start time (unix timestamp). + failed_item_ids: IDs of failed items. + error_summary: Error type counts. + has_more_items: Whether more items exist. + item_evaluations: Dictionary mapping item IDs to their evaluation results. + + Returns: + BatchEvaluationResult instance. + """ + duration = time.time() - start_time + + return BatchEvaluationResult( + total_items_fetched=total_items_fetched, + total_items_processed=total_items_processed, + total_items_failed=total_items_failed, + total_scores_created=total_scores_created, + total_composite_scores_created=total_composite_scores_created, + total_evaluations_failed=total_evaluations_failed, + evaluator_stats=list(evaluator_stats_dict.values()), + resume_token=resume_token, + completed=completed, + duration_seconds=duration, + failed_item_ids=failed_item_ids, + error_summary=error_summary, + has_more_items=has_more_items, + item_evaluations=item_evaluations, + ) diff --git a/langfuse/experiment.py b/langfuse/experiment.py new file mode 100644 index 000000000..404c96e1d --- /dev/null +++ b/langfuse/experiment.py @@ -0,0 +1,1207 @@ +"""Langfuse experiment functionality for running and evaluating tasks on datasets. + +This module provides the core experiment functionality for the Langfuse Python SDK, +allowing users to run experiments on datasets with automatic tracing, evaluation, +and result formatting. +""" + +import asyncio +from datetime import datetime +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Dict, + List, + Optional, + Protocol, + TypedDict, + Union, + overload, +) + +from langfuse.api import DatasetItem +from langfuse.logger import langfuse_logger as logger +from langfuse.types import ExperimentScoreType + +if TYPE_CHECKING: + from langfuse._client.client import Langfuse + from langfuse.batch_evaluation import CompositeEvaluatorFunction + + +class LocalExperimentItem(TypedDict, total=False): + """Structure for local experiment data items (not from Langfuse datasets). + + This TypedDict defines the structure for experiment items when using local data + rather than Langfuse-hosted datasets. All fields are optional to provide + flexibility in data structure. + + Attributes: + input: The input data to pass to the task function. Can be any type that + your task function can process (string, dict, list, etc.). This is + typically the prompt, question, or data that your task will operate on. + expected_output: Optional expected/ground truth output for evaluation purposes. + Used by evaluators to assess correctness or quality. Can be None if + no ground truth is available. + metadata: Optional metadata dictionary containing additional context about + this specific item. Can include information like difficulty level, + category, source, or any other relevant attributes that evaluators + might use for context-aware evaluation. + + Examples: + Simple text processing item: + ```python + item: LocalExperimentItem = { + "input": "Summarize this article: ...", + "expected_output": "Expected summary...", + "metadata": {"difficulty": "medium", "category": "news"} + } + ``` + + Classification item: + ```python + item: LocalExperimentItem = { + "input": {"text": "This movie is great!", "context": "movie review"}, + "expected_output": "positive", + "metadata": {"dataset_source": "imdb", "confidence": 0.95} + } + ``` + + Minimal item with only input: + ```python + item: LocalExperimentItem = { + "input": "What is the capital of France?" + } + ``` + """ + + input: Any + expected_output: Any + metadata: Optional[Dict[str, Any]] + + +ExperimentItem = Union[LocalExperimentItem, DatasetItem] +"""Type alias for items that can be processed in experiments. + +Can be either: +- LocalExperimentItem: Dict-like items with 'input', 'expected_output', 'metadata' keys +- DatasetItem: Items from Langfuse datasets with .input, .expected_output, .metadata attributes +""" + +ExperimentData = Union[List[LocalExperimentItem], List[DatasetItem]] +"""Type alias for experiment datasets. + +Represents the collection of items to process in an experiment. Can be either: +- List[LocalExperimentItem]: Local data items as dictionaries +- List[DatasetItem]: Items from a Langfuse dataset (typically from dataset.items) +""" + + +class Evaluation: + """Represents an evaluation result for an experiment item or an entire experiment run. + + This class provides a strongly-typed way to create evaluation results in evaluator functions. + Users must use keyword arguments when instantiating this class. + + Attributes: + name: Unique identifier for the evaluation metric. Should be descriptive + and consistent across runs (e.g., "accuracy", "bleu_score", "toxicity"). + Used for aggregation and comparison across experiment runs. + value: The evaluation score or result. Can be: + - Numeric (int/float): For quantitative metrics like accuracy (0.85), BLEU (0.42) + - String: For categorical results like "positive", "negative", "neutral" + - Boolean: For binary assessments like "passes_safety_check" + comment: Optional human-readable explanation of the evaluation result. + Useful for providing context, explaining scoring rationale, or noting + special conditions. Displayed in Langfuse UI for interpretability. + metadata: Optional structured metadata about the evaluation process. + Can include confidence scores, intermediate calculations, model versions, + or any other relevant technical details. + data_type: Optional score data type. Required if value is not NUMERIC. + One of NUMERIC, CATEGORICAL, or BOOLEAN. Defaults to NUMERIC. + config_id: Optional Langfuse score config ID. + + Examples: + Basic accuracy evaluation: + ```python + from langfuse import Evaluation + + def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): + if not expected_output: + return Evaluation(name="accuracy", value=0, comment="No expected output") + + is_correct = output.strip().lower() == expected_output.strip().lower() + return Evaluation( + name="accuracy", + value=1.0 if is_correct else 0.0, + comment="Correct answer" if is_correct else "Incorrect answer" + ) + ``` + + Multi-metric evaluator: + ```python + def comprehensive_evaluator(*, input, output, expected_output=None, **kwargs): + return [ + Evaluation(name="length", value=len(output), comment=f"Output length: {len(output)} chars"), + Evaluation(name="has_greeting", value="hello" in output.lower(), comment="Contains greeting"), + Evaluation( + name="quality", + value=0.85, + comment="High quality response", + metadata={"confidence": 0.92, "model": "gpt-4"} + ) + ] + ``` + + Categorical evaluation: + ```python + def sentiment_evaluator(*, input, output, **kwargs): + sentiment = analyze_sentiment(output) # Returns "positive", "negative", or "neutral" + return Evaluation( + name="sentiment", + value=sentiment, + comment=f"Response expresses {sentiment} sentiment", + data_type="CATEGORICAL" + ) + ``` + + Failed evaluation with error handling: + ```python + def external_api_evaluator(*, input, output, **kwargs): + try: + score = external_api.evaluate(output) + return Evaluation(name="external_score", value=score) + except Exception as e: + return Evaluation( + name="external_score", + value=0, + comment=f"API unavailable: {e}", + metadata={"error": str(e), "retry_count": 3} + ) + ``` + + Note: + All arguments must be passed as keywords. Positional arguments are not allowed + to ensure code clarity and prevent errors from argument reordering. + """ + + def __init__( + self, + *, + name: str, + value: Union[int, float, str, bool], + comment: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + data_type: Optional[ExperimentScoreType] = None, + config_id: Optional[str] = None, + ): + """Initialize an Evaluation with the provided data. + + Args: + name: Unique identifier for the evaluation metric. + value: The evaluation score or result. + comment: Optional human-readable explanation of the result. + metadata: Optional structured metadata about the evaluation process. + data_type: Optional score data type (NUMERIC, CATEGORICAL, or BOOLEAN). + config_id: Optional Langfuse score config ID. + + Note: + All arguments must be provided as keywords. Positional arguments will raise a TypeError. + """ + self.name = name + self.value = value + self.comment = comment + self.metadata = metadata + self.data_type = data_type + self.config_id = config_id + + +class ExperimentItemResult: + """Result structure for individual experiment items. + + This class represents the complete result of processing a single item + during an experiment run, including the original input, task output, + evaluations, and tracing information. Users must use keyword arguments when instantiating this class. + + Attributes: + item: The original experiment item that was processed. Can be either + a dictionary with 'input', 'expected_output', and 'metadata' keys, + or a DatasetItem from Langfuse datasets. + output: The actual output produced by the task function for this item. + Can be any type depending on what your task function returns. + evaluations: List of evaluation results for this item. Each evaluation + contains a name, value, optional comment, and optional metadata. + trace_id: Optional Langfuse trace ID for this item's execution. Used + to link the experiment result with the detailed trace in Langfuse UI. + dataset_run_id: Optional dataset run ID if this item was part of a + Langfuse dataset. None for local experiments. + + Examples: + Accessing item result data: + ```python + result = langfuse.run_experiment(...) + for item_result in result.item_results: + print(f"Input: {item_result.item}") + print(f"Output: {item_result.output}") + print(f"Trace: {item_result.trace_id}") + + # Access evaluations + for evaluation in item_result.evaluations: + print(f"{evaluation.name}: {evaluation.value}") + ``` + + Working with different item types: + ```python + # Local experiment item (dict) + if isinstance(item_result.item, dict): + input_data = item_result.item["input"] + expected = item_result.item.get("expected_output") + + # Langfuse dataset item (object with attributes) + else: + input_data = item_result.item.input + expected = item_result.item.expected_output + ``` + + Note: + All arguments must be passed as keywords. Positional arguments are not allowed + to ensure code clarity and prevent errors from argument reordering. + """ + + def __init__( + self, + *, + item: ExperimentItem, + output: Any, + evaluations: List[Evaluation], + trace_id: Optional[str], + dataset_run_id: Optional[str], + ): + """Initialize an ExperimentItemResult with the provided data. + + Args: + item: The original experiment item that was processed. + output: The actual output produced by the task function for this item. + evaluations: List of evaluation results for this item. + trace_id: Optional Langfuse trace ID for this item's execution. + dataset_run_id: Optional dataset run ID if this item was part of a Langfuse dataset. + + Note: + All arguments must be provided as keywords. Positional arguments will raise a TypeError. + """ + self.item = item + self.output = output + self.evaluations = evaluations + self.trace_id = trace_id + self.dataset_run_id = dataset_run_id + + +class ExperimentResult: + """Complete result structure for experiment execution. + + This class encapsulates the complete results of running an experiment on a dataset, + including individual item results, aggregate run-level evaluations, and metadata + about the experiment execution. + + Attributes: + name: The name of the experiment as specified during execution. + run_name: The name of the current experiment run. + description: Optional description of the experiment's purpose or methodology. + item_results: List of results from processing each individual dataset item, + containing the original item, task output, evaluations, and trace information. + run_evaluations: List of aggregate evaluation results computed across all items, + such as average scores, statistical summaries, or cross-item analyses. + experiment_id: ID of the experiment run propagated across all items. For + Langfuse datasets, this matches the dataset run ID. For local experiments, + this is a stable SDK-generated identifier for the run. + dataset_run_id: Optional ID of the dataset run in Langfuse (when using Langfuse datasets). + dataset_run_url: Optional direct URL to view the experiment results in Langfuse UI. + + Examples: + Basic usage with local dataset: + ```python + result = langfuse.run_experiment( + name="Capital Cities Test", + data=local_data, + task=generate_capital, + evaluators=[accuracy_check] + ) + + print(f"Processed {len(result.item_results)} items") + print(result.format()) # Human-readable summary + + # Access individual results + for item_result in result.item_results: + print(f"Input: {item_result.item}") + print(f"Output: {item_result.output}") + print(f"Scores: {item_result.evaluations}") + ``` + + Usage with Langfuse datasets: + ```python + dataset = langfuse.get_dataset("qa-eval-set") + result = dataset.run_experiment( + name="GPT-4 QA Evaluation", + task=answer_question, + evaluators=[relevance_check, accuracy_check] + ) + + # View in Langfuse UI + if result.dataset_run_url: + print(f"View detailed results: {result.dataset_run_url}") + ``` + + Formatted output: + ```python + # Get summary view + summary = result.format() + print(summary) + + # Get detailed view with individual items + detailed = result.format(include_item_results=True) + with open("experiment_report.txt", "w") as f: + f.write(detailed) + ``` + """ + + def __init__( + self, + *, + name: str, + run_name: str, + description: Optional[str], + item_results: List[ExperimentItemResult], + run_evaluations: List[Evaluation], + experiment_id: str, + dataset_run_id: Optional[str] = None, + dataset_run_url: Optional[str] = None, + ): + """Initialize an ExperimentResult with the provided data. + + Args: + name: The name of the experiment. + run_name: The current experiment run name. + description: Optional description of the experiment. + item_results: List of results from processing individual dataset items. + run_evaluations: List of aggregate evaluation results for the entire run. + experiment_id: ID of the experiment run. + dataset_run_id: Optional ID of the dataset run (for Langfuse datasets). + dataset_run_url: Optional URL to view results in Langfuse UI. + """ + self.name = name + self.run_name = run_name + self.description = description + self.item_results = item_results + self.run_evaluations = run_evaluations + self.experiment_id = experiment_id + self.dataset_run_id = dataset_run_id + self.dataset_run_url = dataset_run_url + + def format(self, *, include_item_results: bool = False) -> str: + r"""Format the experiment result for human-readable display. + + Converts the experiment result into a nicely formatted string suitable for + console output, logging, or reporting. The output includes experiment overview, + aggregate statistics, and optionally individual item details. + + This method provides a comprehensive view of experiment performance including: + - Experiment metadata (name, description, item count) + - List of evaluation metrics used across items + - Average scores computed across all processed items + - Run-level evaluation results (aggregate metrics) + - Links to view detailed results in Langfuse UI (when available) + - Individual item details (when requested) + + Args: + include_item_results: Whether to include detailed results for each individual + item in the formatted output. When False (default), only shows aggregate + statistics and summary information. When True, includes input/output/scores + for every processed item, making the output significantly longer but more + detailed for debugging and analysis purposes. + + Returns: + A formatted multi-line string containing: + - Experiment name and description (if provided) + - Total number of items successfully processed + - List of all evaluation metrics that were applied + - Average scores across all items for each numeric metric + - Run-level evaluation results with comments + - Dataset run URL for viewing in Langfuse UI (if applicable) + - Individual item details including inputs, outputs, and scores (if requested) + + Examples: + Basic usage showing aggregate results only: + ```python + result = langfuse.run_experiment( + name="Capital Cities", + data=dataset, + task=generate_capital, + evaluators=[accuracy_evaluator] + ) + + print(result.format()) + # Output: + # ────────────────────────────────────────────────── + # 📊 Capital Cities + # 100 items + # Evaluations: + # • accuracy + # Average Scores: + # • accuracy: 0.850 + ``` + + Detailed output including all individual item results: + ```python + detailed_report = result.format(include_item_results=True) + print(detailed_report) + # Output includes each item: + # 1. Item 1: + # Input: What is the capital of France? + # Expected: Paris + # Actual: The capital of France is Paris. + # Scores: + # • accuracy: 1.000 + # 💭 Correct answer found + # [... continues for all items ...] + ``` + + Saving formatted results to file for reporting: + ```python + with open("experiment_report.txt", "w") as f: + f.write(result.format(include_item_results=True)) + + # Or create summary report + summary = result.format() # Aggregate view only + print(f"Experiment Summary:\n{summary}") + ``` + + Integration with logging systems: + ```python + import logging + logger = logging.getLogger("experiments") + + # Log summary after experiment + logger.info(f"Experiment completed:\n{result.format()}") + + # Log detailed results for failed experiments + if any(eval['value'] < threshold for eval in result.run_evaluations): + logger.warning(f"Poor performance detected:\n{result.format(include_item_results=True)}") + ``` + """ + if not self.item_results: + return "No experiment results to display." + + output = "" + + # Individual results section + if include_item_results: + for i, result in enumerate(self.item_results): + output += f"\n{i + 1}. Item {i + 1}:\n" + + # Extract and display input + item_input = None + if isinstance(result.item, dict): + item_input = result.item.get("input") + elif hasattr(result.item, "input"): + item_input = result.item.input + + if item_input is not None: + output += f" Input: {_format_value(item_input)}\n" + + # Extract and display expected output + expected_output = None + if isinstance(result.item, dict): + expected_output = result.item.get("expected_output") + elif hasattr(result.item, "expected_output"): + expected_output = result.item.expected_output + + if expected_output is not None: + output += f" Expected: {_format_value(expected_output)}\n" + output += f" Actual: {_format_value(result.output)}\n" + + # Display evaluation scores + if result.evaluations: + output += " Scores:\n" + for evaluation in result.evaluations: + score = evaluation.value + if isinstance(score, (int, float)): + score = f"{score:.3f}" + output += f" • {evaluation.name}: {score}" + if evaluation.comment: + output += f"\n 💭 {evaluation.comment}" + output += "\n" + + # Display trace link if available + if result.trace_id: + output += f"\n Trace ID: {result.trace_id}\n" + else: + output += f"Individual Results: Hidden ({len(self.item_results)} items)\n" + output += "💡 Set include_item_results=True to view them\n" + + # Experiment overview section + output += f"\n{'─' * 50}\n" + output += f"🧪 Experiment: {self.name}" + output += f"\n📋 Run name: {self.run_name}" + if self.description: + output += f" - {self.description}" + + output += f"\n{len(self.item_results)} items" + + # Collect unique evaluation names across all items + evaluation_names = set() + for result in self.item_results: + for evaluation in result.evaluations: + evaluation_names.add(evaluation.name) + + if evaluation_names: + output += "\nEvaluations:" + for eval_name in evaluation_names: + output += f"\n • {eval_name}" + output += "\n" + + # Calculate and display average scores + if evaluation_names: + output += "\nAverage Scores:" + for eval_name in evaluation_names: + scores = [] + for result in self.item_results: + for evaluation in result.evaluations: + if evaluation.name == eval_name and isinstance( + evaluation.value, (int, float) + ): + scores.append(evaluation.value) + + if scores: + avg = sum(scores) / len(scores) + output += f"\n • {eval_name}: {avg:.3f}" + output += "\n" + + # Display run-level evaluations + if self.run_evaluations: + output += "\nRun Evaluations:" + for run_eval in self.run_evaluations: + score = run_eval.value + if isinstance(score, (int, float)): + score = f"{score:.3f}" + output += f"\n • {run_eval.name}: {score}" + if run_eval.comment: + output += f"\n 💭 {run_eval.comment}" + output += "\n" + + # Add dataset run URL if available + if self.dataset_run_url: + output += f"\n🔗 Dataset Run:\n {self.dataset_run_url}" + + return output + + +class TaskFunction(Protocol): + """Protocol defining the interface for experiment task functions. + + Task functions are the core processing functions that operate on each item + in an experiment dataset. They receive an experiment item as input and + produce some output that will be evaluated. + + Task functions must: + - Accept 'item' as a keyword argument + - Return any type of output (will be passed to evaluators) + - Can be either synchronous or asynchronous + - Should handle their own errors gracefully (exceptions will be logged) + """ + + def __call__( + self, + *, + item: ExperimentItem, + **kwargs: Dict[str, Any], + ) -> Union[Any, Awaitable[Any]]: + """Execute the task on an experiment item. + + This method defines the core processing logic for each item in your experiment. + The implementation should focus on the specific task you want to evaluate, + such as text generation, classification, summarization, etc. + + Args: + item: The experiment item to process. Can be either: + - Dict with keys like 'input', 'expected_output', 'metadata' + - Langfuse DatasetItem object with .input, .expected_output attributes + **kwargs: Additional keyword arguments that may be passed by the framework + + Returns: + Any: The output of processing the item. This output will be: + - Stored in the experiment results + - Passed to all item-level evaluators for assessment + - Traced automatically in Langfuse for observability + + Can return either a direct value or an awaitable (async) result. + + Examples: + Simple synchronous task: + ```python + def my_task(*, item, **kwargs): + prompt = f"Summarize: {item['input']}" + return my_llm_client.generate(prompt) + ``` + + Async task with error handling: + ```python + async def my_async_task(*, item, **kwargs): + try: + response = await openai_client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": item["input"]}] + ) + return response.choices[0].message.content + except Exception as e: + # Log error and return fallback + print(f"Task failed for item {item}: {e}") + return "Error: Could not process item" + ``` + + Task using dataset item attributes: + ```python + def classification_task(*, item, **kwargs): + # Works with both dict items and DatasetItem objects + text = item["input"] if isinstance(item, dict) else item.input + return classify_text(text) + ``` + """ + ... + + +class EvaluatorFunction(Protocol): + """Protocol defining the interface for item-level evaluator functions. + + Item-level evaluators assess the quality, correctness, or other properties + of individual task outputs. They receive the input, output, expected output, + and metadata for each item and return evaluation metrics. + + Evaluators should: + - Accept input, output, expected_output, and metadata as keyword arguments + - Return Evaluation dict(s) with 'name', 'value', 'comment', 'metadata' fields + - Be deterministic when possible for reproducible results + - Handle edge cases gracefully (missing expected output, malformed data, etc.) + - Can be either synchronous or asynchronous + """ + + def __call__( + self, + *, + input: Any, + output: Any, + expected_output: Any, + metadata: Optional[Dict[str, Any]], + **kwargs: Dict[str, Any], + ) -> Union[ + Evaluation, List[Evaluation], Awaitable[Union[Evaluation, List[Evaluation]]] + ]: + r"""Evaluate a task output for quality, correctness, or other metrics. + + This method should implement specific evaluation logic such as accuracy checking, + similarity measurement, toxicity detection, fluency assessment, etc. + + Args: + input: The original input that was passed to the task function. + This is typically the item['input'] or item.input value. + output: The output produced by the task function for this input. + This is the direct return value from your task function. + expected_output: The expected/ground truth output for comparison. + May be None if not available in the dataset. Evaluators should + handle this case appropriately. + metadata: Optional metadata from the experiment item that might + contain additional context for evaluation (categories, difficulty, etc.) + **kwargs: Additional keyword arguments that may be passed by the framework + + Returns: + Evaluation results in one of these formats: + - Single Evaluation dict: {"name": "accuracy", "value": 0.85, "comment": "..."} + - List of Evaluation dicts: [{"name": "precision", ...}, {"name": "recall", ...}] + - Awaitable returning either of the above (for async evaluators) + + Each Evaluation dict should contain: + - name (str): Unique identifier for this evaluation metric + - value (int|float|str|bool): The evaluation score or result + - comment (str, optional): Human-readable explanation of the result + - metadata (dict, optional): Additional structured data about the evaluation + + Examples: + Simple accuracy evaluator: + ```python + def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): + if expected_output is None: + return {"name": "accuracy", "value": 0, "comment": "No expected output"} + + is_correct = output.strip().lower() == expected_output.strip().lower() + return { + "name": "accuracy", + "value": 1.0 if is_correct else 0.0, + "comment": "Exact match" if is_correct else "No match" + } + ``` + + Multi-metric evaluator: + ```python + def comprehensive_evaluator(*, input, output, expected_output=None, **kwargs): + results = [] + + # Length check + results.append({ + "name": "output_length", + "value": len(output), + "comment": f"Output contains {len(output)} characters" + }) + + # Sentiment analysis + sentiment_score = analyze_sentiment(output) + results.append({ + "name": "sentiment", + "value": sentiment_score, + "comment": f"Sentiment score: {sentiment_score:.2f}" + }) + + return results + ``` + + Async evaluator using external API: + ```python + async def llm_judge_evaluator(*, input, output, expected_output=None, **kwargs): + prompt = f"Rate the quality of this response on a scale of 1-10:\n" + prompt += f"Question: {input}\nResponse: {output}" + + response = await openai_client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": prompt}] + ) + + try: + score = float(response.choices[0].message.content.strip()) + return { + "name": "llm_judge_quality", + "value": score, + "comment": f"LLM judge rated this {score}/10" + } + except ValueError: + return { + "name": "llm_judge_quality", + "value": 0, + "comment": "Could not parse LLM judge score" + } + ``` + + Context-aware evaluator: + ```python + def context_evaluator(*, input, output, metadata=None, **kwargs): + # Use metadata for context-specific evaluation + difficulty = metadata.get("difficulty", "medium") if metadata else "medium" + + # Adjust expectations based on difficulty + min_length = {"easy": 50, "medium": 100, "hard": 150}[difficulty] + + meets_requirement = len(output) >= min_length + return { + "name": f"meets_{difficulty}_requirement", + "value": meets_requirement, + "comment": f"Output {'meets' if meets_requirement else 'fails'} {difficulty} length requirement" + } + ``` + """ + ... + + +class RunEvaluatorFunction(Protocol): + """Protocol defining the interface for run-level evaluator functions. + + Run-level evaluators assess aggregate properties of the entire experiment run, + computing metrics that span across all items rather than individual outputs. + They receive the complete results from all processed items and can compute + statistics like averages, distributions, correlations, or other aggregate metrics. + + Run evaluators should: + - Accept item_results as a keyword argument containing all item results + - Return Evaluation dict(s) with aggregate metrics + - Handle cases where some items may have failed processing + - Compute meaningful statistics across the dataset + - Can be either synchronous or asynchronous + """ + + def __call__( + self, + *, + item_results: List[ExperimentItemResult], + **kwargs: Dict[str, Any], + ) -> Union[ + Evaluation, List[Evaluation], Awaitable[Union[Evaluation, List[Evaluation]]] + ]: + r"""Evaluate the entire experiment run with aggregate metrics. + + This method should implement aggregate evaluation logic such as computing + averages, calculating distributions, finding correlations, detecting patterns + across items, or performing statistical analysis on the experiment results. + + Args: + item_results: List of results from all successfully processed experiment items. + Each item result contains: + - item: The original experiment item + - output: The task function's output for this item + - evaluations: List of item-level evaluation results + - trace_id: Langfuse trace ID for this execution + - dataset_run_id: Dataset run ID (if using Langfuse datasets) + + Note: This list only includes items that were successfully processed. + Failed items are excluded but logged separately. + **kwargs: Additional keyword arguments that may be passed by the framework + + Returns: + Evaluation results in one of these formats: + - Single Evaluation dict: {"name": "avg_accuracy", "value": 0.78, "comment": "..."} + - List of Evaluation dicts: [{"name": "mean", ...}, {"name": "std_dev", ...}] + - Awaitable returning either of the above (for async evaluators) + + Each Evaluation dict should contain: + - name (str): Unique identifier for this run-level metric + - value (int|float|str|bool): The aggregate evaluation result + - comment (str, optional): Human-readable explanation of the metric + - metadata (dict, optional): Additional structured data about the evaluation + + Examples: + Average accuracy calculator: + ```python + def average_accuracy(*, item_results, **kwargs): + if not item_results: + return {"name": "avg_accuracy", "value": 0.0, "comment": "No results"} + + accuracy_values = [] + for result in item_results: + for evaluation in result.evaluations: + if evaluation.name == "accuracy": + accuracy_values.append(evaluation.value) + + if not accuracy_values: + return {"name": "avg_accuracy", "value": 0, "comment": "No accuracy evaluations found"} + + avg = sum(accuracy_values) / len(accuracy_values) + return { + "name": "avg_accuracy", + "value": avg, + "comment": f"Average accuracy across {len(accuracy_values)} items: {avg:.2%}" + } + ``` + + Multiple aggregate metrics: + ```python + def statistical_summary(*, item_results, **kwargs): + if not item_results: + return [] + + results = [] + + # Calculate output length statistics + lengths = [len(str(result.output)) for result in item_results] + results.extend([ + {"name": "avg_output_length", "value": sum(lengths) / len(lengths)}, + {"name": "min_output_length", "value": min(lengths)}, + {"name": "max_output_length", "value": max(lengths)} + ]) + + # Success rate + total_items = len(item_results) # Only successful items are included + results.append({ + "name": "processing_success_rate", + "value": 1.0, # All items in item_results succeeded + "comment": f"Successfully processed {total_items} items" + }) + + return results + ``` + + Async run evaluator with external analysis: + ```python + async def llm_batch_analysis(*, item_results, **kwargs): + # Prepare batch analysis prompt + outputs = [result.output for result in item_results] + prompt = f"Analyze these {len(outputs)} outputs for common themes:\n" + prompt += "\n".join(f"{i+1}. {output}" for i, output in enumerate(outputs)) + + response = await openai_client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": prompt}] + ) + + return { + "name": "thematic_analysis", + "value": response.choices[0].message.content, + "comment": f"LLM analysis of {len(outputs)} outputs" + } + ``` + + Performance distribution analysis: + ```python + def performance_distribution(*, item_results, **kwargs): + # Extract all evaluation scores + all_scores = [] + score_by_metric = {} + + for result in item_results: + for evaluation in result.evaluations: + metric_name = evaluation.name + value = evaluation.value + + if isinstance(value, (int, float)): + all_scores.append(value) + if metric_name not in score_by_metric: + score_by_metric[metric_name] = [] + score_by_metric[metric_name].append(value) + + results = [] + + # Overall score distribution + if all_scores: + import statistics + results.append({ + "name": "score_std_dev", + "value": statistics.stdev(all_scores) if len(all_scores) > 1 else 0, + "comment": f"Standard deviation across all numeric scores" + }) + + # Per-metric statistics + for metric, scores in score_by_metric.items(): + if len(scores) > 1: + results.append({ + "name": f"{metric}_variance", + "value": statistics.variance(scores), + "comment": f"Variance in {metric} across {len(scores)} items" + }) + + return results + ``` + """ + ... + + +def _format_value(value: Any) -> str: + """Format a value for display.""" + if isinstance(value, str): + return value[:50] + "..." if len(value) > 50 else value + return str(value) + + +async def _run_evaluator( + evaluator: Union[EvaluatorFunction, RunEvaluatorFunction], **kwargs: Any +) -> List[Evaluation]: + """Run an evaluator function and normalize the result.""" + try: + result = evaluator(**kwargs) + + # Handle async evaluators + if asyncio.iscoroutine(result): + result = await result + + # Normalize to list + if isinstance(result, (dict, Evaluation)): + return [result] # type: ignore + + elif isinstance(result, list): + return result + + else: + return [] + + except Exception as e: + evaluator_name = getattr(evaluator, "__name__", "unknown_evaluator") + logger.error(f"Evaluator {evaluator_name} failed: {e}") + return [] + + +async def _run_task(task: TaskFunction, item: ExperimentItem) -> Any: + """Run a task function and handle sync/async.""" + result = task(item=item) + + # Handle async tasks + if asyncio.iscoroutine(result): + result = await result + + return result + + +def create_evaluator_from_autoevals( + autoevals_evaluator: Any, **kwargs: Optional[Dict[str, Any]] +) -> EvaluatorFunction: + """Create a Langfuse evaluator from an autoevals evaluator. + + Args: + autoevals_evaluator: An autoevals evaluator instance + **kwargs: Additional arguments passed to the evaluator + + Returns: + A Langfuse-compatible evaluator function + """ + + def langfuse_evaluator( + *, + input: Any, + output: Any, + expected_output: Any, + metadata: Optional[Dict[str, Any]], + **langfuse_kwargs: Dict[str, Any], + ) -> Evaluation: + evaluation = autoevals_evaluator( + input=input, output=output, expected=expected_output, **kwargs + ) + + return Evaluation( + name=evaluation.name, + value=evaluation.score, + comment=(evaluation.metadata or {}).get("comment"), + metadata=evaluation.metadata, + ) + + return langfuse_evaluator + + +class RunnerContext: + """Wraps :meth:`Langfuse.run_experiment` with CI-injected defaults. + + Intended for use with the ``langfuse/experiment-action`` GitHub Action + (https://github.com/langfuse/experiment-action). The action builds a + ``RunnerContext`` before invoking the user's ``experiment(context)`` + function. Defaults set here (dataset, metadata tags) are applied when + the user omits them on the :meth:`run_experiment` call; users can + override any default by passing the corresponding argument explicitly. + """ + + def __init__( + self, + *, + client: "Langfuse", + data: Optional[ExperimentData] = None, + dataset_version: Optional[datetime] = None, + metadata: Optional[Dict[str, str]] = None, + ): + """Build a ``RunnerContext`` populated with defaults for ``run_experiment``. + + Typically called by the ``langfuse/experiment-action`` GitHub Action, + not by end users directly. Every field except ``client`` is optional: + fields left as ``None`` simply mean the corresponding argument must be + supplied on the :meth:`run_experiment` call. + + Args: + client: Initialized Langfuse SDK client used to execute the + experiment. The action creates this from the + ``langfuse_public_key`` / ``langfuse_secret_key`` / + ``langfuse_base_url`` inputs. + data: Default dataset items to run the experiment on. Accepts + either ``List[LocalExperimentItem]`` or ``List[DatasetItem]``. + Injected by the action when ``dataset_name`` is configured. + If ``None``, the user must pass ``data=`` to + :meth:`run_experiment`. + dataset_version: Optional pinned dataset version. Injected by the + action when ``dataset_version`` is configured. + metadata: Default metadata attached to every experiment trace and + the dataset run. The action injects GitHub-sourced tags (SHA, + PR link, workflow run link, branch, GH user, etc.). Merged + with any ``metadata`` passed to :meth:`run_experiment`, with + user-supplied keys winning on collision. + """ + self.client = client + self.data = data + self.dataset_version = dataset_version + self.metadata = metadata + + def run_experiment( + self, + *, + name: str, + run_name: Optional[str] = None, + description: Optional[str] = None, + data: Optional[ExperimentData] = None, + task: TaskFunction, + evaluators: List[EvaluatorFunction] = [], + composite_evaluator: Optional["CompositeEvaluatorFunction"] = None, + run_evaluators: List[RunEvaluatorFunction] = [], + max_concurrency: int = 50, + metadata: Optional[Dict[str, str]] = None, + _dataset_version: Optional[datetime] = None, + ) -> ExperimentResult: + resolved_data = data if data is not None else self.data + if resolved_data is None: + raise ValueError( + "`data` must be provided either on the RunnerContext or the run_experiment call" + ) + + resolved_dataset_version = ( + _dataset_version if _dataset_version is not None else self.dataset_version + ) + + merged_metadata: Optional[Dict[str, str]] + if self.metadata is None and metadata is None: + merged_metadata = None + else: + merged_metadata = {**(self.metadata or {}), **(metadata or {})} + + return self.client.run_experiment( + name=name, + run_name=run_name, + description=description, + data=resolved_data, + task=task, + evaluators=evaluators, + composite_evaluator=composite_evaluator, + run_evaluators=run_evaluators, + max_concurrency=max_concurrency, + metadata=merged_metadata, + _dataset_version=resolved_dataset_version, + ) + + +class RegressionError(Exception): + """Raised by a user's ``experiment`` function to signal a CI gate failure. + + Intended for use with the ``langfuse/experiment-action`` GitHub Action + (https://github.com/langfuse/experiment-action). The action catches this + exception and, when ``should_fail_on_error`` is enabled, fails the + workflow run and renders a callout in the PR comment using + ``metric``/``value``/``threshold`` if supplied, otherwise ``str(exc)``. + + Callers choose one of three forms: + + - ``RegressionError(result=r)`` — minimal, generic message. + - ``RegressionError(result=r, message="...")`` — free-form message. + - ``RegressionError(result=r, metric="acc", value=0.7, threshold=0.9)`` — + structured; ``metric`` and ``value`` must be provided together so the + action can render a targeted callout without ``None`` placeholders. + """ + + @overload + def __init__(self, *, result: ExperimentResult) -> None: ... + @overload + def __init__(self, *, result: ExperimentResult, message: str) -> None: ... + @overload + def __init__( + self, + *, + result: ExperimentResult, + metric: str, + value: float, + threshold: Optional[float] = None, + message: Optional[str] = None, + ) -> None: ... + def __init__( + self, + *, + result: ExperimentResult, + metric: Optional[str] = None, + value: Optional[float] = None, + threshold: Optional[float] = None, + message: Optional[str] = None, + ): + self.result = result + self.metric = metric + self.value = value + self.threshold = threshold + if message is not None: + formatted = message + elif metric is not None and value is not None: + formatted = f"Regression on `{metric}`: {value} (threshold {threshold})" + else: + formatted = "Experiment regression detected" + super().__init__(formatted) diff --git a/langfuse/langchain/CallbackHandler.py b/langfuse/langchain/CallbackHandler.py index ed7cfb70a..1349f6ae0 100644 --- a/langfuse/langchain/CallbackHandler.py +++ b/langfuse/langchain/CallbackHandler.py @@ -1,44 +1,84 @@ -import typing +from collections import OrderedDict +from contextvars import Token +from dataclasses import dataclass, field +from typing import ( + Any, + Dict, + List, + Literal, + Optional, + Sequence, + Set, + Type, + Union, + cast, +) +from uuid import UUID import pydantic +from opentelemetry import context, trace +from opentelemetry.util._decorator import _AgnosticContextManager +from langfuse import propagate_attributes +from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse._client.client import Langfuse from langfuse._client.get_client import get_client -from langfuse._client.span import LangfuseGeneration, LangfuseSpan +from langfuse._client.propagation import _detach_context_token_safely +from langfuse._client.span import ( + LangfuseAgent, + LangfuseChain, + LangfuseGeneration, + LangfuseRetriever, + LangfuseSpan, + LangfuseTool, +) +from langfuse._utils import _get_timestamp +from langfuse.langchain.utils import _extract_model_name from langfuse.logger import langfuse_logger +from langfuse.types import TraceContext try: - import langchain # noqa - -except ImportError as e: - langfuse_logger.error( - f"Could not import langchain. The langchain integration will not work. {e}" - ) + import langchain -from typing import Any, Dict, List, Literal, Optional, Sequence, Set, Type, Union, cast -from uuid import UUID + if langchain.__version__.startswith("1"): + # Langchain v1 + from langchain_core.agents import AgentAction, AgentFinish + from langchain_core.callbacks import ( + BaseCallbackHandler as LangchainBaseCallbackHandler, + ) + from langchain_core.documents import Document + from langchain_core.messages import ( + AIMessage, + BaseMessage, + ChatMessage, + FunctionMessage, + HumanMessage, + SystemMessage, + ToolMessage, + ) + from langchain_core.outputs import ChatGeneration, LLMResult -from langfuse._utils import _get_timestamp -from langfuse.langchain.utils import _extract_model_name + else: + # Langchain v0 + from langchain.callbacks.base import ( # type: ignore + BaseCallbackHandler as LangchainBaseCallbackHandler, + ) + from langchain.schema.agent import AgentAction, AgentFinish # type: ignore + from langchain.schema.document import Document # type: ignore + from langchain_core.messages import ( + AIMessage, + BaseMessage, + ChatMessage, + FunctionMessage, + HumanMessage, + SystemMessage, + ToolMessage, + ) + from langchain_core.outputs import ( + ChatGeneration, + LLMResult, + ) -try: - from langchain.callbacks.base import ( - BaseCallbackHandler as LangchainBaseCallbackHandler, - ) - from langchain.schema.agent import AgentAction, AgentFinish - from langchain.schema.document import Document - from langchain_core.messages import ( - AIMessage, - BaseMessage, - ChatMessage, - FunctionMessage, - HumanMessage, - SystemMessage, - ToolMessage, - ) - from langchain_core.outputs import ( - ChatGeneration, - LLMResult, - ) except ImportError: raise ModuleNotFoundError( "Please install langchain to use the Langfuse langchain integration: 'pip install langchain'" @@ -46,6 +86,8 @@ LANGSMITH_TAG_HIDDEN: str = "langsmith:hidden" CONTROL_FLOW_EXCEPTION_TYPES: Set[Type[BaseException]] = set() +LANGGRAPH_COMMAND_TYPE: Optional[Type[Any]] = None +MAX_PENDING_RESUME_TRACE_CONTEXTS = 1024 try: from langgraph.errors import GraphBubbleUp @@ -54,14 +96,98 @@ except ImportError: pass +try: + from langgraph.types import Command as LangGraphCommand + + LANGGRAPH_COMMAND_TYPE = LangGraphCommand +except ImportError: + pass + + +@dataclass +class _RunState: + parent_run_id: Optional[UUID] + root_run_id: UUID + + +@dataclass +class _RootRunState: + run_ids: Set[UUID] = field(default_factory=set) + resume_key: Optional[str] = None + propagation_context_manager: Optional[_AgnosticContextManager] = None + + +class _PendingResumeTraceContextStore: + def __init__(self, max_size: int) -> None: + self._max_size = max_size + self._contexts: OrderedDict[str, TraceContext] = OrderedDict() + + def store(self, *, resume_key: str, trace_context: TraceContext) -> None: + self._contexts[resume_key] = trace_context + self._contexts.move_to_end(resume_key) + + if len(self._contexts) > self._max_size: + self._contexts.popitem(last=False) + + def take(self, resume_key: str) -> Optional[TraceContext]: + return self._contexts.pop(resume_key, None) + + def __contains__(self, resume_key: str) -> bool: + return resume_key in self._contexts + + def __len__(self) -> int: + return len(self._contexts) + + def keys(self) -> List[str]: + return list(self._contexts.keys()) + class LangchainCallbackHandler(LangchainBaseCallbackHandler): - def __init__(self, *, public_key: Optional[str] = None) -> None: - self.client = get_client(public_key=public_key) + def __init__( + self, + *, + public_key: Optional[str] = None, + trace_context: Optional[TraceContext] = None, + ) -> None: + """Initialize the LangchainCallbackHandler. + + Args: + public_key: Optional Langfuse public key. If not provided, will use the default client configuration. + trace_context: Optional context for connecting to an existing trace (distributed tracing) or + setting a custom trace id for the root LangChain run. Pass a `TraceContext` dict, e.g. + `{"trace_id": ""}` (and optionally `{"parent_span_id": ""}`) to link + the trace to an upstream system. + + Example: + Use a custom trace id without context managers: - self.runs: Dict[UUID, Union[LangfuseSpan, LangfuseGeneration]] = {} - self.prompt_to_parent_run_map: Dict[UUID, Any] = {} - self.updated_completion_start_time_memo: Set[UUID] = set() + ```python + from langfuse.langchain import CallbackHandler + + handler = CallbackHandler(trace_context={"trace_id": "my-trace-id"}) + ``` + """ + self._langfuse_client = get_client(public_key=public_key) + self._runs: Dict[ + UUID, + Union[ + LangfuseSpan, + LangfuseGeneration, + LangfuseAgent, + LangfuseChain, + LangfuseTool, + LangfuseRetriever, + ], + ] = {} + self._context_tokens: Dict[UUID, Token] = {} + self._prompt_to_parent_run_map: Dict[UUID, Any] = {} + self._updated_completion_start_time_memo: Set[UUID] = set() + self._trace_context = trace_context + self._pending_resume_trace_contexts = _PendingResumeTraceContextStore( + MAX_PENDING_RESUME_TRACE_CONTEXTS + ) + self._run_states: Dict[UUID, _RunState] = {} + self._root_run_states: Dict[UUID, _RootRunState] = {} self.last_trace_id: Optional[str] = None @@ -78,14 +204,207 @@ def on_llm_new_token( f"on llm new token: run_id: {run_id} parent_run_id: {parent_run_id}" ) if ( - run_id in self.runs - and isinstance(self.runs[run_id], LangfuseGeneration) - and run_id not in self.updated_completion_start_time_memo + run_id in self._runs + and isinstance(self._runs[run_id], LangfuseGeneration) + and run_id not in self._updated_completion_start_time_memo ): - current_generation = cast(LangfuseGeneration, self.runs[run_id]) + current_generation = cast(LangfuseGeneration, self._runs[run_id]) current_generation.update(completion_start_time=_get_timestamp()) - self.updated_completion_start_time_memo.add(run_id) + self._updated_completion_start_time_memo.add(run_id) + + def _get_langgraph_resume_key( + self, metadata: Optional[Dict[str, Any]] + ) -> Optional[str]: + thread_id = metadata.get("thread_id") if metadata else None + + if thread_id is None: + return None + + return str(thread_id) + + def _track_run( + self, + *, + run_id: UUID, + parent_run_id: Optional[UUID], + metadata: Optional[Dict[str, Any]] = None, + ) -> None: + if run_id in self._run_states: + return + + if parent_run_id is None: + root_run_id = run_id + self._root_run_states[root_run_id] = _RootRunState( + run_ids={run_id}, + resume_key=self._get_langgraph_resume_key(metadata), + ) + else: + parent_state = self._run_states.get(parent_run_id) + root_run_id = ( + parent_state.root_run_id if parent_state is not None else parent_run_id + ) + root_run_state = self._root_run_states.setdefault( + root_run_id, _RootRunState() + ) + root_run_state.run_ids.add(run_id) + + self._run_states[run_id] = _RunState( + parent_run_id=parent_run_id, + root_run_id=root_run_id, + ) + + def _get_run_state(self, run_id: UUID) -> Optional[_RunState]: + return self._run_states.get(run_id) + + def _get_root_run_state(self, run_id: UUID) -> Optional[_RootRunState]: + run_state = self._get_run_state(run_id) + + if run_state is None: + return None + + return self._root_run_states.get(run_state.root_run_id) + + def _pop_root_run_resume_key(self, run_id: UUID) -> Optional[str]: + root_run_state = self._get_root_run_state(run_id) + + if root_run_state is None: + return None + + resume_key = root_run_state.resume_key + root_run_state.resume_key = None + + return resume_key + + def _get_parent_run_id(self, run_id: UUID) -> Optional[UUID]: + run_state = self._get_run_state(run_id) + return run_state.parent_run_id if run_state is not None else None + + def _is_langgraph_resume(self, inputs: Any) -> bool: + return ( + LANGGRAPH_COMMAND_TYPE is not None + and isinstance(inputs, LANGGRAPH_COMMAND_TYPE) + and getattr(inputs, "resume", None) is not None + ) + + def _store_resume_trace_context( + self, *, resume_key: str, trace_context: TraceContext + ) -> None: + self._pending_resume_trace_contexts.store( + resume_key=resume_key, trace_context=trace_context + ) + + def _take_root_trace_context( + self, *, inputs: Any, metadata: Optional[Dict[str, Any]] + ) -> tuple[Optional[str], Optional[TraceContext]]: + if self._trace_context is not None: + return None, self._trace_context + + current_span_context = trace.get_current_span().get_span_context() + + # Only reuse the pending resume context when this callback run has no active + # parent span of its own. Nested callbacks should attach normally. + if current_span_context.is_valid: + return None, None + + # Only explicit LangGraph resumes should consume pending trace linkage. + if not self._is_langgraph_resume(inputs): + return None, None + + resume_key = self._get_langgraph_resume_key(metadata) + if resume_key is None: + return None, None + + return resume_key, self._pending_resume_trace_contexts.take(resume_key) + + def _restore_root_trace_context( + self, *, resume_key: Optional[str], trace_context: Optional[TraceContext] + ) -> None: + if self._trace_context is not None: + return + + if resume_key is None or trace_context is None: + return + + # Span creation failed after we consumed the pending linkage, so put it + # back and let the next retry resume the interrupted trace correctly. + self._store_resume_trace_context( + resume_key=resume_key, trace_context=trace_context + ) + + def _clear_root_run_resume_key(self, run_id: UUID) -> None: + # Keep the pending interrupt context until an explicit Command(resume=...) + # arrives. A separate root run on the same thread_id is not a resume. + self._pop_root_run_resume_key(run_id) + + def _persist_resume_trace_context(self, *, run_id: UUID, observation: Any) -> None: + if self._trace_context is not None: + return + + resume_key = self._pop_root_run_resume_key(run_id) + if resume_key is None: + return + + self._store_resume_trace_context( + resume_key=resume_key, + trace_context={ + "trace_id": observation.trace_id, + "parent_span_id": observation.id, + }, + ) + + def _get_error_level_and_status_message( + self, error: BaseException + ) -> tuple[Literal["DEFAULT", "ERROR"], str]: + # LangGraph uses GraphBubbleUp subclasses for expected control flow such as + # interrupts and handoffs, so they should stay visible without being errors. + if any(isinstance(error, t) for t in CONTROL_FLOW_EXCEPTION_TYPES): + return "DEFAULT", str(error) or type(error).__name__ + + return "ERROR", str(error) + + def _get_observation_type_from_serialized( + self, serialized: Optional[Dict[str, Any]], callback_type: str, **kwargs: Any + ) -> Union[ + Literal["tool"], + Literal["retriever"], + Literal["generation"], + Literal["agent"], + Literal["chain"], + Literal["span"], + ]: + """Determine Langfuse observation type from LangChain component. + + Args: + serialized: LangChain's serialized component dict + callback_type: The type of callback (e.g., "chain", "tool", "retriever", "llm") + **kwargs: Additional keyword arguments from the callback + + Returns: + The appropriate Langfuse observation type string + """ + # Direct mappings based on callback type + if callback_type == "tool": + return "tool" + elif callback_type == "retriever": + return "retriever" + elif callback_type == "llm": + return "generation" + elif callback_type == "chain": + # Detect if it's an agent by examining class path or name + if serialized and "id" in serialized: + class_path = serialized["id"] + if any("agent" in part.lower() for part in class_path): + return "agent" + + # Check name for agent-related keywords + name = self.get_langchain_run_name(serialized, **kwargs) + if "agent" in name.lower(): + return "agent" + + return "chain" + + return "span" def get_langchain_run_name( self, serialized: Optional[Dict[str, Any]], **kwargs: Any @@ -136,23 +455,101 @@ def on_retriever_error( self._log_debug_event( "on_retriever_error", run_id, parent_run_id, error=error ) + observation = self._detach_observation(run_id) - if run_id is None or run_id not in self.runs: - raise Exception("run not found") + if observation is not None: + level, status_message = self._get_error_level_and_status_message(error) + observation.update( + level=cast( + Optional[Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"]], + level, + ), + status_message=status_message, + input=kwargs.get("inputs"), + cost_details={"total": 0}, + ).end() - self.runs[run_id].update( - level="ERROR", - status_message=str(error), - input=kwargs.get("inputs"), - ).end() + if parent_run_id is None and level == "DEFAULT": + self._persist_resume_trace_context( + run_id=run_id, observation=observation + ) + elif parent_run_id is None: + self._clear_root_run_resume_key(run_id) except Exception as e: langfuse_logger.exception(e) + finally: + if parent_run_id is None: + self._reset(run_id) + + def _parse_langfuse_trace_attributes( + self, *, metadata: Optional[Dict[str, Any]], tags: Optional[List[str]] + ) -> Dict[str, Any]: + attributes: Dict[str, Any] = {} + + if metadata is None and tags is not None: + return {"tags": tags} + + if metadata is None: + return attributes + + if "langfuse_session_id" in metadata and isinstance( + metadata["langfuse_session_id"], str + ): + attributes["session_id"] = metadata["langfuse_session_id"] + + if "langfuse_user_id" in metadata and isinstance( + metadata["langfuse_user_id"], str + ): + attributes["user_id"] = metadata["langfuse_user_id"] + + if "langfuse_trace_name" in metadata and isinstance( + metadata["langfuse_trace_name"], str + ): + attributes["trace_name"] = metadata["langfuse_trace_name"] + + if tags is not None or ( + "langfuse_tags" in metadata and isinstance(metadata["langfuse_tags"], list) + ): + langfuse_tags = ( + metadata["langfuse_tags"] + if "langfuse_tags" in metadata + and isinstance(metadata["langfuse_tags"], list) + else [] + ) + merged_tags = list(set(langfuse_tags) | set(tags or [])) + attributes["tags"] = [str(tag) for tag in set(merged_tags)] + + attributes["metadata"] = _strip_langfuse_keys_from_dict(metadata, False) + + return attributes + + def _get_langchain_observation_metadata( + self, + *, + parent_run_id: Optional[UUID], + tags: Optional[List[str]] = None, + metadata: Optional[Dict[str, Any]] = None, + keep_langfuse_trace_attributes: bool = False, + ) -> Optional[Dict[str, Any]]: + observation_metadata = self.__join_tags_and_metadata( + tags=tags, + metadata=metadata, + keep_langfuse_trace_attributes=keep_langfuse_trace_attributes, + ) + + if parent_run_id is not None: + return observation_metadata + + root_metadata = observation_metadata.copy() if observation_metadata else {} + root_metadata["is_langchain_root"] = True + + return root_metadata def on_chain_start( self, serialized: Optional[Dict[str, Any]], - inputs: Dict[str, Any], + inputs: Any, *, run_id: UUID, parent_run_id: Optional[UUID] = None, @@ -160,6 +557,12 @@ def on_chain_start( metadata: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Any: + self._track_run(run_id=run_id, parent_run_id=parent_run_id, metadata=metadata) + + span = None + resume_key = None + trace_context = None + try: self._log_debug_event( "on_chain_start", run_id, parent_run_id, inputs=inputs @@ -169,35 +572,79 @@ def on_chain_start( ) span_name = self.get_langchain_run_name(serialized, **kwargs) - span_metadata = self.__join_tags_and_metadata(tags, metadata) + span_metadata = self._get_langchain_observation_metadata( + parent_run_id=parent_run_id, + tags=tags, + metadata=metadata, + ) span_level = "DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None + observation_type = self._get_observation_type_from_serialized( + serialized, "chain", **kwargs + ) + + # Handle trace attribute propagation at the root of the chain if parent_run_id is None: - self.runs[run_id] = self.client.start_span( + parsed_trace_attributes = self._parse_langfuse_trace_attributes( + metadata=metadata, tags=tags + ) + + propagation_context_manager = propagate_attributes( + user_id=parsed_trace_attributes.get("user_id", None), + session_id=parsed_trace_attributes.get("session_id", None), + tags=parsed_trace_attributes.get("tags", None), + metadata=parsed_trace_attributes.get("metadata", None), + trace_name=parsed_trace_attributes.get("trace_name", None), + ) + + root_run_state = self._get_root_run_state(run_id) + if root_run_state is not None: + root_run_state.propagation_context_manager = ( + propagation_context_manager + ) + + propagation_context_manager.__enter__() + + obs = self._get_parent_observation(parent_run_id) + if isinstance(obs, Langfuse): + resume_key, trace_context = self._take_root_trace_context( + inputs=inputs, metadata=metadata + ) + span = obs.start_observation( + trace_context=trace_context, name=span_name, + as_type=observation_type, metadata=span_metadata, input=inputs, level=cast( - Optional[Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"]], + Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"] | None, span_level, ), ) else: - self.runs[run_id] = cast( - LangfuseSpan, self.runs[parent_run_id] - ).start_span( + span = obs.start_observation( name=span_name, + as_type=observation_type, metadata=span_metadata, input=inputs, level=cast( - Optional[Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"]], + Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"] | None, span_level, ), ) - self.last_trace_id = self.runs[run_id].trace_id + self._attach_observation(run_id, span) + + self.last_trace_id = self._runs[run_id].trace_id except Exception as e: + if span is None: + self._restore_root_trace_context( + resume_key=resume_key, trace_context=trace_context + ) + if parent_run_id is None: + self._exit_propagation_context(run_id) + self._reset(run_id) langfuse_logger.exception(e) def _register_langfuse_prompt( @@ -212,23 +659,86 @@ def _register_langfuse_prompt( If parent_run_id is None, we are at the root of a trace and should not attempt to register the prompt, as there will be no LLM invocation following it. Otherwise it would have been traced in with a parent run consisting of the prompt template formatting and the LLM invocation. """ - if not parent_run_id: + if not parent_run_id or not run_id: return langfuse_prompt = metadata and metadata.get("langfuse_prompt", None) if langfuse_prompt: - self.prompt_to_parent_run_map[parent_run_id] = langfuse_prompt + self._prompt_to_parent_run_map[parent_run_id] = langfuse_prompt # If we have a registered prompt that has not been linked to a generation yet, we need to allow _children_ of that chain to link to it. # Otherwise, we only allow generations on the same level of the prompt rendering to be linked, not if they are nested. - elif parent_run_id in self.prompt_to_parent_run_map: - registered_prompt = self.prompt_to_parent_run_map[parent_run_id] - self.prompt_to_parent_run_map[run_id] = registered_prompt + elif parent_run_id in self._prompt_to_parent_run_map: + registered_prompt = self._prompt_to_parent_run_map[parent_run_id] + self._prompt_to_parent_run_map[run_id] = registered_prompt def _deregister_langfuse_prompt(self, run_id: Optional[UUID]) -> None: - if run_id in self.prompt_to_parent_run_map: - del self.prompt_to_parent_run_map[run_id] + if run_id is not None and run_id in self._prompt_to_parent_run_map: + del self._prompt_to_parent_run_map[run_id] + + def _get_parent_observation( + self, parent_run_id: Optional[UUID] + ) -> Union[ + Langfuse, + LangfuseAgent, + LangfuseChain, + LangfuseGeneration, + LangfuseRetriever, + LangfuseSpan, + LangfuseTool, + ]: + if parent_run_id and parent_run_id in self._runs: + return self._runs[parent_run_id] + + return self._langfuse_client + + def _attach_observation( + self, + run_id: UUID, + observation: Union[ + LangfuseAgent, + LangfuseChain, + LangfuseGeneration, + LangfuseRetriever, + LangfuseSpan, + LangfuseTool, + ], + ) -> None: + ctx = trace.set_span_in_context(observation._otel_span) + token = context.attach(ctx) + + self._runs[run_id] = observation + self._context_tokens[run_id] = token + + def _detach_observation( + self, run_id: UUID + ) -> Optional[ + Union[ + LangfuseAgent, + LangfuseChain, + LangfuseGeneration, + LangfuseRetriever, + LangfuseSpan, + LangfuseTool, + ] + ]: + token = self._context_tokens.pop(run_id, None) + + if token: + _detach_context_token_safely(token) + + return cast( + Union[ + LangfuseAgent, + LangfuseChain, + LangfuseGeneration, + LangfuseRetriever, + LangfuseSpan, + LangfuseTool, + ], + self._runs.pop(run_id, None), + ) def on_agent_action( self, @@ -239,18 +749,24 @@ def on_agent_action( **kwargs: Any, ) -> Any: """Run on agent action.""" + self._track_run(run_id=run_id, parent_run_id=parent_run_id) + try: self._log_debug_event( "on_agent_action", run_id, parent_run_id, action=action ) - if run_id not in self.runs: - raise Exception("run not found") + agent_run = self._runs.get(run_id, None) + + if agent_run is not None: + agent_run._otel_span.set_attribute( + LangfuseOtelSpanAttributes.OBSERVATION_TYPE, "agent" + ) - self.runs[run_id].update( - output=action, - input=kwargs.get("inputs"), - ).end() + agent_run.update( + output=action, + input=kwargs.get("inputs"), + ) except Exception as e: langfuse_logger.exception(e) @@ -267,13 +783,19 @@ def on_agent_finish( self._log_debug_event( "on_agent_finish", run_id, parent_run_id, finish=finish ) - if run_id not in self.runs: - raise Exception("run not found") + # Langchain is sending same run ID for both agent finish and chain end + # handle cleanup of observation in the chain end callback + agent_run = self._runs.get(run_id, None) - self.runs[run_id].update( - output=finish, - input=kwargs.get("inputs"), - ).end() + if agent_run is not None: + agent_run._otel_span.set_attribute( + LangfuseOtelSpanAttributes.OBSERVATION_TYPE, "agent" + ) + + agent_run.update( + output=finish, + input=kwargs.get("inputs"), + ) except Exception as e: langfuse_logger.exception(e) @@ -291,20 +813,30 @@ def on_chain_end( "on_chain_end", run_id, parent_run_id, outputs=outputs ) - if run_id not in self.runs: - raise Exception("run not found") + span = self._detach_observation(run_id) - self.runs[run_id].update( - output=outputs, - input=kwargs.get("inputs"), - ).end() + if span is not None: + span.update( + output=outputs, + input=kwargs.get("inputs"), + ) + + if parent_run_id is None: + self._clear_root_run_resume_key(run_id) + self._exit_propagation_context(run_id) - del self.runs[run_id] + span.end() + + self._deregister_langfuse_prompt(run_id) - self._deregister_langfuse_prompt(run_id) except Exception as e: langfuse_logger.exception(e) + finally: + if parent_run_id is None: + self._exit_propagation_context(run_id) + self._reset(run_id) + def on_chain_error( self, error: BaseException, @@ -316,28 +848,38 @@ def on_chain_error( ) -> None: try: self._log_debug_event("on_chain_error", run_id, parent_run_id, error=error) - if run_id in self.runs: - if any(isinstance(error, t) for t in CONTROL_FLOW_EXCEPTION_TYPES): - level = None - else: - level = "ERROR" + level, status_message = self._get_error_level_and_status_message(error) + + observation = self._detach_observation(run_id) - self.runs[run_id].update( + if observation is not None: + observation.update( level=cast( - Optional[Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"]], level + Optional[Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"]], + level, ), - status_message=str(error) if level else None, + status_message=status_message, input=kwargs.get("inputs"), - ).end() - - del self.runs[run_id] - else: - langfuse_logger.warning( - f"Run ID {run_id} already popped from run map. Could not update run with error message" + cost_details={"total": 0}, ) + if parent_run_id is None: + if level == "DEFAULT": + self._persist_resume_trace_context( + run_id=run_id, observation=observation + ) + else: + self._clear_root_run_resume_key(run_id) + self._exit_propagation_context(run_id) + + observation.end() + except Exception as e: langfuse_logger.exception(e) + finally: + if parent_run_id is None: + self._exit_propagation_context(run_id) + self._reset(run_id) def on_chat_model_start( self, @@ -350,6 +892,8 @@ def on_chat_model_start( metadata: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Any: + self._track_run(run_id=run_id, parent_run_id=parent_run_id, metadata=metadata) + try: self._log_debug_event( "on_chat_model_start", run_id, parent_run_id, messages=messages @@ -382,6 +926,8 @@ def on_llm_start( metadata: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Any: + self._track_run(run_id=run_id, parent_run_id=parent_run_id, metadata=metadata) + try: self._log_debug_event( "on_llm_start", run_id, parent_run_id, prompts=prompts @@ -409,14 +955,18 @@ def on_tool_start( metadata: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Any: + self._track_run(run_id=run_id, parent_run_id=parent_run_id, metadata=metadata) + try: self._log_debug_event( "on_tool_start", run_id, parent_run_id, input_str=input_str ) - if parent_run_id is None or parent_run_id not in self.runs: - raise Exception("parent run not found") - meta = self.__join_tags_and_metadata(tags, metadata) + meta = self._get_langchain_observation_metadata( + parent_run_id=parent_run_id, + tags=tags, + metadata=metadata, + ) if not meta: meta = {} @@ -425,13 +975,31 @@ def on_tool_start( {key: value for key, value in kwargs.items() if value is not None} ) - self.runs[run_id] = cast(LangfuseSpan, self.runs[parent_run_id]).start_span( - name=self.get_langchain_run_name(serialized, **kwargs), - input=input_str, - metadata=meta, - level="DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None, + observation_type = self._get_observation_type_from_serialized( + serialized, "tool", **kwargs ) + parent_observation = self._get_parent_observation(parent_run_id) + if isinstance(parent_observation, Langfuse): + span = parent_observation.start_observation( + trace_context=self._trace_context, + name=self.get_langchain_run_name(serialized, **kwargs), + as_type=observation_type, + input=input_str, + metadata=meta, + level="DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None, + ) + else: + span = parent_observation.start_observation( + name=self.get_langchain_run_name(serialized, **kwargs), + as_type=observation_type, + input=input_str, + metadata=meta, + level="DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None, + ) + + self._attach_observation(run_id, span) + except Exception as e: langfuse_logger.exception(e) @@ -446,17 +1014,29 @@ def on_retriever_start( metadata: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Any: + self._track_run(run_id=run_id, parent_run_id=parent_run_id, metadata=metadata) + try: self._log_debug_event( "on_retriever_start", run_id, parent_run_id, query=query ) span_name = self.get_langchain_run_name(serialized, **kwargs) - span_metadata = self.__join_tags_and_metadata(tags, metadata) + span_metadata = self._get_langchain_observation_metadata( + parent_run_id=parent_run_id, + tags=tags, + metadata=metadata, + ) span_level = "DEBUG" if tags and LANGSMITH_TAG_HIDDEN in tags else None - if parent_run_id is None: - self.runs[run_id] = self.client.start_span( + observation_type = self._get_observation_type_from_serialized( + serialized, "retriever", **kwargs + ) + parent_observation = self._get_parent_observation(parent_run_id) + if isinstance(parent_observation, Langfuse): + span = parent_observation.start_observation( + trace_context=self._trace_context, name=span_name, + as_type=observation_type, metadata=span_metadata, input=query, level=cast( @@ -465,18 +1045,19 @@ def on_retriever_start( ), ) else: - self.runs[run_id] = cast( - LangfuseSpan, self.runs[parent_run_id] - ).start_span( + span = parent_observation.start_observation( name=span_name, - input=query, + as_type=observation_type, metadata=span_metadata, + input=query, level=cast( Optional[Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"]], span_level, ), ) + self._attach_observation(run_id, span) + except Exception as e: langfuse_logger.exception(e) @@ -492,18 +1073,21 @@ def on_retriever_end( self._log_debug_event( "on_retriever_end", run_id, parent_run_id, documents=documents ) - if run_id is None or run_id not in self.runs: - raise Exception("run not found") - - self.runs[run_id].update( - output=documents, - input=kwargs.get("inputs"), - ).end() + observation = self._detach_observation(run_id) - del self.runs[run_id] + if observation is not None: + if parent_run_id is None: + self._clear_root_run_resume_key(run_id) + observation.update( + output=documents, + input=kwargs.get("inputs"), + ).end() except Exception as e: langfuse_logger.exception(e) + finally: + if parent_run_id is None: + self._reset(run_id) def on_tool_end( self, @@ -515,18 +1099,22 @@ def on_tool_end( ) -> Any: try: self._log_debug_event("on_tool_end", run_id, parent_run_id, output=output) - if run_id is None or run_id not in self.runs: - raise Exception("run not found") - self.runs[run_id].update( - output=output, - input=kwargs.get("inputs"), - ).end() + observation = self._detach_observation(run_id) - del self.runs[run_id] + if observation is not None: + if parent_run_id is None: + self._clear_root_run_resume_key(run_id) + observation.update( + output=output, + input=kwargs.get("inputs"), + ).end() except Exception as e: langfuse_logger.exception(e) + finally: + if parent_run_id is None: + self._reset(run_id) def on_tool_error( self, @@ -538,19 +1126,32 @@ def on_tool_error( ) -> Any: try: self._log_debug_event("on_tool_error", run_id, parent_run_id, error=error) - if run_id is None or run_id not in self.runs: - raise Exception("run not found") + observation = self._detach_observation(run_id) - self.runs[run_id].update( - status_message=str(error), - level="ERROR", - input=kwargs.get("inputs"), - ).end() + if observation is not None: + level, status_message = self._get_error_level_and_status_message(error) + observation.update( + status_message=status_message, + level=cast( + Optional[Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"]], + level, + ), + input=kwargs.get("inputs"), + cost_details={"total": 0}, + ).end() - del self.runs[run_id] + if parent_run_id is None and level == "DEFAULT": + self._persist_resume_trace_context( + run_id=run_id, observation=observation + ) + elif parent_run_id is None: + self._clear_root_run_resume_key(run_id) except Exception as e: langfuse_logger.exception(e) + finally: + if parent_run_id is None: + self._reset(run_id) def __on_llm_action( self, @@ -562,6 +1163,8 @@ def __on_llm_action( metadata: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> None: + self._track_run(run_id=run_id, parent_run_id=parent_run_id, metadata=metadata) + try: tools = kwargs.get("invocation_params", {}).get("tools", None) if tools and isinstance(tools, list): @@ -570,32 +1173,55 @@ def __on_llm_action( model_name = self._parse_model_and_log_errors( serialized=serialized, metadata=metadata, kwargs=kwargs ) - registered_prompt = ( - self.prompt_to_parent_run_map.get(parent_run_id) - if parent_run_id is not None - else None - ) - if registered_prompt: - self._deregister_langfuse_prompt(parent_run_id) + registered_prompt = None + current_parent_run_id = parent_run_id + + # Check all parents for registered prompt + while current_parent_run_id is not None: + registered_prompt = self._prompt_to_parent_run_map.get( + current_parent_run_id + ) + + if registered_prompt: + self._deregister_langfuse_prompt(current_parent_run_id) + break + else: + current_parent_run_id = self._get_parent_run_id( + current_parent_run_id + ) content = { "name": self.get_langchain_run_name(serialized, **kwargs), "input": prompts, - "metadata": self.__join_tags_and_metadata(tags, metadata), + "metadata": self._get_langchain_observation_metadata( + parent_run_id=parent_run_id, + tags=tags, + metadata=metadata, + # If llm is run isolated and outside chain, keep trace attributes + keep_langfuse_trace_attributes=True + if parent_run_id is None + else False, + ), "model": model_name, "model_parameters": self._parse_model_parameters(kwargs), "prompt": registered_prompt, } - if parent_run_id is not None and parent_run_id in self.runs: - self.runs[run_id] = cast( - LangfuseSpan, self.runs[parent_run_id] - ).start_generation(**content) # type: ignore + parent_observation = self._get_parent_observation(parent_run_id) + if isinstance(parent_observation, Langfuse): + generation = parent_observation.start_observation( + trace_context=self._trace_context, + as_type="generation", + **content, + ) # type: ignore else: - self.runs[run_id] = self.client.start_generation(**content) # type: ignore + generation = parent_observation.start_observation( + as_type="generation", **content + ) # type: ignore + self._attach_observation(run_id, generation) - self.last_trace_id = self.runs[run_id].trace_id + self.last_trace_id = self._runs[run_id].trace_id except Exception as e: langfuse_logger.exception(e) @@ -675,37 +1301,38 @@ def on_llm_end( self._log_debug_event( "on_llm_end", run_id, parent_run_id, response=response, kwargs=kwargs ) - if run_id not in self.runs: - raise Exception("Run not found, see docs what to do in this case.") - else: - response_generation = response.generations[-1][-1] - extracted_response = ( - self._convert_message_to_dict(response_generation.message) - if isinstance(response_generation, ChatGeneration) - else _extract_raw_response(response_generation) - ) + response_generation = response.generations[-1][-1] + extracted_response = ( + self._convert_message_to_dict(response_generation.message) + if isinstance(response_generation, ChatGeneration) + else _extract_raw_response(response_generation) + ) + + llm_usage = _parse_usage(response) - llm_usage = _parse_usage(response) + # e.g. azure returns the model name in the response + model = _parse_model(response) - # e.g. azure returns the model name in the response - model = _parse_model(response) - langfuse_generation = cast(LangfuseGeneration, self.runs[run_id]) - langfuse_generation.update( + generation = self._detach_observation(run_id) + + if generation is not None: + generation.update( output=extracted_response, usage=llm_usage, usage_details=llm_usage, input=kwargs.get("inputs"), model=model, - ) - langfuse_generation.end() - - del self.runs[run_id] + ).end() except Exception as e: langfuse_logger.exception(e) finally: - self.updated_completion_start_time_memo.discard(run_id) + self._updated_completion_start_time_memo.discard(run_id) + + if parent_run_id is None: + self._clear_root_run_resume_key(run_id) + self._reset(run_id) def on_llm_error( self, @@ -717,41 +1344,99 @@ def on_llm_error( ) -> Any: try: self._log_debug_event("on_llm_error", run_id, parent_run_id, error=error) - if run_id in self.runs: - generation = self.runs[run_id] + + generation = self._detach_observation(run_id) + + if generation is not None: + level, status_message = self._get_error_level_and_status_message(error) generation.update( - status_message=str(error), - level="ERROR", + status_message=status_message, + level=cast( + Optional[Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"]], + level, + ), input=kwargs.get("inputs"), - ) - generation.end() + cost_details={"total": 0}, + ).end() - del self.runs[run_id] + if parent_run_id is None and level == "DEFAULT": + self._persist_resume_trace_context( + run_id=run_id, observation=generation + ) + elif parent_run_id is None: + self._clear_root_run_resume_key(run_id) except Exception as e: langfuse_logger.exception(e) + finally: + if parent_run_id is None: + self._reset(run_id) + + def _reset(self, root_run_id: UUID) -> None: + run_state = self._get_run_state(root_run_id) + if run_state is None: + return + + root_run_state = self._root_run_states.pop(run_state.root_run_id, None) + if root_run_state is None: + self._run_states.pop(root_run_id, None) + return + + for run_id in root_run_state.run_ids: + self._run_states.pop(run_id, None) + + def _exit_propagation_context(self, run_id: UUID) -> None: + root_run_state = self._get_root_run_state(run_id) + + if root_run_state is None: + return + + manager = root_run_state.propagation_context_manager + if manager is None: + return + + root_run_state.propagation_context_manager = None + manager.__exit__(None, None, None) def __join_tags_and_metadata( self, tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None, - trace_metadata: Optional[Dict[str, Any]] = None, + keep_langfuse_trace_attributes: bool = False, ) -> Optional[Dict[str, Any]]: final_dict = {} if tags is not None and len(tags) > 0: final_dict["tags"] = tags if metadata is not None: final_dict.update(metadata) - if trace_metadata is not None: - final_dict.update(trace_metadata) - return _strip_langfuse_keys_from_dict(final_dict) if final_dict != {} else None + + return ( + _strip_langfuse_keys_from_dict(final_dict, keep_langfuse_trace_attributes) + if final_dict != {} + else None + ) def _convert_message_to_dict(self, message: BaseMessage) -> Dict[str, Any]: # assistant message if isinstance(message, HumanMessage): - message_dict = {"role": "user", "content": message.content} + message_dict: Dict[str, Any] = {"role": "user", "content": message.content} elif isinstance(message, AIMessage): message_dict = {"role": "assistant", "content": message.content} + + if ( + hasattr(message, "tool_calls") + and message.tool_calls is not None + and len(message.tool_calls) > 0 + ): + message_dict["tool_calls"] = message.tool_calls + + if ( + hasattr(message, "invalid_tool_calls") + and message.invalid_tool_calls is not None + and len(message.invalid_tool_calls) > 0 + ): + message_dict["invalid_tool_calls"] = message.invalid_tool_calls + elif isinstance(message, SystemMessage): message_dict = {"role": "system", "content": message.content} elif isinstance(message, ToolMessage): @@ -787,7 +1472,7 @@ def _log_debug_event( **kwargs: Any, ) -> None: langfuse_logger.debug( - f"Event: {event_name}, run_id: {str(run_id)[:5]}, parent_run_id: {str(parent_run_id)[:5]}" + f"Event: {event_name}, run_id: {run_id}, parent_run_id: {parent_run_id}" ) @@ -808,7 +1493,7 @@ def _flatten_comprehension(matrix: Any) -> Any: return [item for row in matrix for item in row] -def _parse_usage_model(usage: typing.Union[pydantic.BaseModel, dict]) -> Any: +def _parse_usage_model(usage: Union[pydantic.BaseModel, dict]) -> Any: # maintains a list of key translations. For each key, the usage model is checked # and a new object will be created with the new key if the key exists in the usage model # All non matched keys will remain on the object. @@ -821,6 +1506,9 @@ def _parse_usage_model(usage: typing.Union[pydantic.BaseModel, dict]) -> Any: ("input_tokens", "input"), ("output_tokens", "output"), ("total_tokens", "total"), + # ChatBedrock API follows a separate format compared to ChatBedrockConverse API + ("prompt_tokens", "input"), + ("completion_tokens", "output"), # https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/get-token-count ("prompt_token_count", "input"), ("candidates_token_count", "output"), @@ -837,22 +1525,41 @@ def _parse_usage_model(usage: typing.Union[pydantic.BaseModel, dict]) -> Any: usage_model = cast(Dict, usage.copy()) # Copy all existing key-value pairs # Skip OpenAI usage types as they are handled server side - if not all( - openai_key in usage_model - for openai_key in ["prompt_tokens", "completion_tokens", "total_tokens"] + if ( + all( + openai_key in usage_model + for openai_key in [ + "prompt_tokens", + "completion_tokens", + "total_tokens", + "prompt_tokens_details", + "completion_tokens_details", + ] + ) + and len(usage_model.keys()) == 5 + ) or ( + all( + openai_key in usage_model + for openai_key in [ + "prompt_tokens", + "completion_tokens", + "total_tokens", + ] + ) + and len(usage_model.keys()) == 3 ): - for model_key, langfuse_key in conversion_list: - if model_key in usage_model: - captured_count = usage_model.pop(model_key) - final_count = ( - sum(captured_count) - if isinstance(captured_count, list) - else captured_count - ) # For Bedrock, the token count is a list when streamed - - usage_model[langfuse_key] = ( - final_count # Translate key and keep the value - ) + return usage_model + + for model_key, langfuse_key in conversion_list: + if model_key in usage_model: + captured_count = usage_model.pop(model_key) + final_count = ( + sum(captured_count) + if isinstance(captured_count, list) + else captured_count + ) # For Bedrock, the token count is a list when streamed + + usage_model[langfuse_key] = final_count # Translate key and keep the value if isinstance(usage_model, dict): if "input_token_details" in usage_model: @@ -861,6 +1568,10 @@ def _parse_usage_model(usage: typing.Union[pydantic.BaseModel, dict]) -> Any: for key, value in input_token_details.items(): usage_model[f"input_{key}"] = value + # Skip priority-tier keys as they are not exclusive sub-categories + if key == "priority" or key.startswith("priority_"): + continue + if "input" in usage_model: usage_model["input"] = max(0, usage_model["input"] - value) @@ -870,6 +1581,10 @@ def _parse_usage_model(usage: typing.Union[pydantic.BaseModel, dict]) -> Any: for key, value in output_token_details.items(): usage_model[f"output_{key}"] = value + # Skip priority-tier keys as they are not exclusive sub-categories + if key == "priority" or key.startswith("priority_"): + continue + if "output" in usage_model: usage_model["output"] = max(0, usage_model["output"] - value) @@ -927,7 +1642,12 @@ def _parse_usage_model(usage: typing.Union[pydantic.BaseModel, dict]) -> Any: if "input" in usage_model: usage_model["input"] = max(0, usage_model["input"] - value) - usage_model = {k: v for k, v in usage_model.items() if not isinstance(v, str)} + if f"input_modality_{item['modality']}" in usage_model: + usage_model[f"input_modality_{item['modality']}"] = max( + 0, usage_model[f"input_modality_{item['modality']}"] - value + ) + + usage_model = {k: v for k, v in usage_model.items() if isinstance(v, int)} return usage_model if usage_model else None @@ -951,7 +1671,9 @@ def _parse_usage(response: LLMResult) -> Any: llm_usage = _parse_usage_model( generation_chunk.generation_info["usage_metadata"] ) - break + + if llm_usage is not None: + break message_chunk = getattr(generation_chunk, "message", {}) response_metadata = getattr(message_chunk, "response_metadata", {}) @@ -999,15 +1721,30 @@ def _parse_model_name_from_metadata(metadata: Optional[Dict[str, Any]]) -> Any: return metadata.get("ls_model_name", None) -def _strip_langfuse_keys_from_dict(metadata: Optional[Dict[str, Any]]) -> Any: +def _strip_langfuse_keys_from_dict( + metadata: Optional[Dict[str, Any]], keep_langfuse_trace_attributes: bool +) -> Any: if metadata is None or not isinstance(metadata, dict): return metadata - langfuse_metadata_keys = ["langfuse_prompt"] + langfuse_metadata_keys = [ + "langfuse_prompt", + ] + + langfuse_trace_attribute_keys = [ + "langfuse_session_id", + "langfuse_user_id", + "langfuse_tags", + "langfuse_trace_name", + ] metadata_copy = metadata.copy() for key in langfuse_metadata_keys: metadata_copy.pop(key, None) + if not keep_langfuse_trace_attributes: + for key in langfuse_trace_attribute_keys: + metadata_copy.pop(key, None) + return metadata_copy diff --git a/langfuse/media.py b/langfuse/media.py index 6691785af..53940382c 100644 --- a/langfuse/media.py +++ b/langfuse/media.py @@ -2,19 +2,19 @@ import base64 import hashlib -import logging import os import re from typing import TYPE_CHECKING, Any, Literal, Optional, Tuple, TypeVar, cast -import requests - -if TYPE_CHECKING: - from langfuse._client.client import Langfuse +import httpx from langfuse.api import MediaContentType +from langfuse.logger import langfuse_logger as logger from langfuse.types import ParsedMediaReference +if TYPE_CHECKING: + from langfuse._client.client import Langfuse + T = TypeVar("T") @@ -40,7 +40,6 @@ class LangfuseMedia: obj: object - _log = logging.getLogger(__name__) _content_bytes: Optional[bytes] _content_type: Optional[MediaContentType] _source: Optional[str] @@ -87,7 +86,7 @@ def __init__( self._content_type = content_type if self._content_bytes else None self._source = "file" if self._content_bytes else None else: - self._log.error( + logger.error( "base64_data_uri, or content_bytes and content_type, or file_path must be provided to LangfuseMedia" ) @@ -102,7 +101,7 @@ def _read_file(self, file_path: str) -> Optional[bytes]: with open(file_path, "rb") as file: return file.read() except Exception as e: - self._log.error(f"Error reading file at path {file_path}", exc_info=e) + logger.error(f"Error reading file at path {file_path}", exc_info=e) return None @@ -218,7 +217,7 @@ def _parse_base64_data_uri( return base64.b64decode(actual_data), cast(MediaContentType, content_type) except Exception as e: - self._log.error("Error parsing base64 data URI", exc_info=e) + logger.error("Error parsing base64 data URI", exc_info=e) return None, None @@ -284,6 +283,11 @@ def traverse(obj: Any, depth: int) -> Any: result = obj reference_string_to_media_content = {} + httpx_client = ( + langfuse_client._resources.httpx_client + if langfuse_client._resources is not None + else None + ) for reference_string in reference_string_matches: try: @@ -293,11 +297,17 @@ def traverse(obj: Any, depth: int) -> Any: media_data = langfuse_client.api.media.get( parsed_media_reference["media_id"] ) - media_content = requests.get( - media_data.url, timeout=content_fetch_timeout_seconds + media_content = ( + httpx_client.get( + media_data.url, + timeout=content_fetch_timeout_seconds, + ) + if httpx_client is not None + else httpx.get( + media_data.url, timeout=content_fetch_timeout_seconds + ) ) - if not media_content.ok: - raise Exception("Failed to fetch media content") + media_content.raise_for_status() base64_media_content = base64.b64encode( media_content.content @@ -308,7 +318,7 @@ def traverse(obj: Any, depth: int) -> Any: base64_data_uri ) except Exception as e: - LangfuseMedia._log.warning( + logger.warning( f"Error fetching media content for reference string {reference_string}: {e}" ) # Do not replace the reference string if there's an error diff --git a/langfuse/model.py b/langfuse/model.py index d1b5a80cf..69d721597 100644 --- a/langfuse/model.py +++ b/langfuse/model.py @@ -4,52 +4,23 @@ from abc import ABC, abstractmethod from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple, TypedDict, Union -from langfuse.api.resources.commons.types.dataset import ( - Dataset, # noqa: F401 +from langfuse.api import ( + CreateDatasetItemRequest, # noqa + CreateDatasetRequest, # noqa + Dataset, # noqa + DatasetItem, # noqa + DatasetRun, # noqa + DatasetStatus, # noqa + MapValue, # noqa + Observation, # noqa + Prompt, + Prompt_Chat, + Prompt_Text, + TraceWithFullDetails, # noqa ) - -# these imports need to stay here, otherwise imports from our clients wont work -from langfuse.api.resources.commons.types.dataset_item import DatasetItem # noqa: F401 - -# noqa: F401 -from langfuse.api.resources.commons.types.dataset_run import DatasetRun # noqa: F401 - -# noqa: F401 -from langfuse.api.resources.commons.types.dataset_status import ( # noqa: F401 - DatasetStatus, -) -from langfuse.api.resources.commons.types.map_value import MapValue # noqa: F401 -from langfuse.api.resources.commons.types.observation import Observation # noqa: F401 -from langfuse.api.resources.commons.types.trace_with_full_details import ( # noqa: F401 - TraceWithFullDetails, -) - -# noqa: F401 -from langfuse.api.resources.dataset_items.types.create_dataset_item_request import ( # noqa: F401 - CreateDatasetItemRequest, -) -from langfuse.api.resources.dataset_run_items.types.create_dataset_run_item_request import ( # noqa: F401 - CreateDatasetRunItemRequest, -) - -# noqa: F401 -from langfuse.api.resources.datasets.types.create_dataset_request import ( # noqa: F401 - CreateDatasetRequest, -) -from langfuse.api.resources.prompts import Prompt, Prompt_Chat, Prompt_Text from langfuse.logger import langfuse_logger -class ModelUsage(TypedDict): - unit: Optional[str] - input: Optional[int] - output: Optional[int] - total: Optional[int] - input_cost: Optional[float] - output_cost: Optional[float] - total_cost: Optional[float] - - class ChatMessageDict(TypedDict): role: str content: str @@ -165,7 +136,13 @@ def compile( self, **kwargs: Union[str, Any] ) -> Union[ str, - Sequence[Union[ChatMessageDict, ChatMessageWithPlaceholdersDict_Placeholder]], + Sequence[ + Union[ + Dict[str, Any], + ChatMessageDict, + ChatMessageWithPlaceholdersDict_Placeholder, + ] + ], ]: pass @@ -327,7 +304,11 @@ def __init__(self, prompt: Prompt_Chat, is_fallback: bool = False): def compile( self, **kwargs: Union[str, Any], - ) -> Sequence[Union[ChatMessageDict, ChatMessageWithPlaceholdersDict_Placeholder]]: + ) -> Sequence[ + Union[ + Dict[str, Any], ChatMessageDict, ChatMessageWithPlaceholdersDict_Placeholder + ] + ]: """Compile the prompt with placeholders and variables. Args: @@ -338,7 +319,11 @@ def compile( List of compiled chat messages as plain dictionaries, with unresolved placeholders kept as-is. """ compiled_messages: List[ - Union[ChatMessageDict, ChatMessageWithPlaceholdersDict_Placeholder] + Union[ + Dict[str, Any], + ChatMessageDict, + ChatMessageWithPlaceholdersDict_Placeholder, + ] ] = [] unresolved_placeholders: List[ChatMessageWithPlaceholdersDict_Placeholder] = [] @@ -361,20 +346,18 @@ def compile( placeholder_value = kwargs[placeholder_name] if isinstance(placeholder_value, list): for msg in placeholder_value: - if ( - isinstance(msg, dict) - and "role" in msg - and "content" in msg - ): - compiled_messages.append( - ChatMessageDict( - role=msg["role"], # type: ignore - content=TemplateParser.compile_template( - msg["content"], # type: ignore - kwargs, - ), - ), + if isinstance(msg, dict): + # Preserve all fields from the original message, such as tool calls + compiled_msg = dict(msg) # type: ignore + # Ensure role and content are always present + compiled_msg["role"] = msg.get("role", "NOT_GIVEN") + compiled_msg["content"] = ( + TemplateParser.compile_template( + msg.get("content", ""), # type: ignore + kwargs, + ) ) + compiled_messages.append(compiled_msg) else: compiled_messages.append( ChatMessageDict( diff --git a/langfuse/openai.py b/langfuse/openai.py index b8d9ea2d7..96fd55ce0 100644 --- a/langfuse/openai.py +++ b/langfuse/openai.py @@ -17,13 +17,12 @@ See docs for more details: https://langfuse.com/docs/integrations/openai """ -import logging import types from collections import defaultdict from dataclasses import dataclass from datetime import datetime -from inspect import isclass -from typing import Optional, cast, Any +from inspect import isawaitable, isclass +from typing import Any, Optional, cast from openai._types import NotGiven from packaging.version import Version @@ -33,25 +32,17 @@ from langfuse._client.get_client import get_client from langfuse._client.span import LangfuseGeneration from langfuse._utils import _get_timestamp +from langfuse.logger import langfuse_logger as logger from langfuse.media import LangfuseMedia try: import openai + from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI # noqa: F401 except ImportError: raise ModuleNotFoundError( "Please install OpenAI to use this feature: 'pip install openai'" ) -try: - from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI # noqa: F401 -except ImportError: - AsyncAzureOpenAI = None # type: ignore - AsyncOpenAI = None # type: ignore - AzureOpenAI = None # type: ignore - OpenAI = None # type: ignore - -log = logging.getLogger("langfuse") - @dataclass class OpenAiDefinition: @@ -161,6 +152,36 @@ class OpenAiDefinition: sync=False, min_version="1.66.0", ), + OpenAiDefinition( + module="openai.resources.responses", + object="Responses", + method="parse", + type="chat", + sync=True, + min_version="1.66.0", + ), + OpenAiDefinition( + module="openai.resources.responses", + object="AsyncResponses", + method="parse", + type="chat", + sync=False, + min_version="1.66.0", + ), + OpenAiDefinition( + module="openai.resources.embeddings", + object="Embeddings", + method="create", + type="embedding", + sync=True, + ), + OpenAiDefinition( + module="openai.resources.embeddings", + object="AsyncEmbeddings", + method="create", + type="embedding", + sync=False, + ), ] @@ -225,6 +246,34 @@ def wrapper(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any: return _with_langfuse +def _extract_responses_prompt(kwargs: Any) -> Any: + input_value = kwargs.get("input", None) + instructions = kwargs.get("instructions", None) + + if isinstance(input_value, NotGiven): + input_value = None + + if isinstance(instructions, NotGiven): + instructions = None + + if instructions is None: + return input_value + + if input_value is None: + return {"instructions": instructions} + + if isinstance(input_value, str): + return [ + {"role": "system", "content": instructions}, + {"role": "user", "content": input_value}, + ] + + if isinstance(input_value, list): + return [{"role": "system", "content": instructions}, *input_value] + + return {"instructions": instructions, "input": input_value} + + def _extract_chat_prompt(kwargs: Any) -> Any: """Extracts the user input from prompts. Returns an array of messages or dict with messages and functions""" prompt = {} @@ -324,10 +373,13 @@ def _extract_chat_response(kwargs: Any) -> Any: def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> Any: - name = kwargs.get("name", "OpenAI-generation") + default_name = ( + "OpenAI-embedding" if resource.type == "embedding" else "OpenAI-generation" + ) + name = kwargs.get("name", default_name) if name is None: - name = "OpenAI-generation" + name = default_name if name is not None and not isinstance(name, str): raise TypeError("name must be a string") @@ -367,7 +419,10 @@ def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> A and not isinstance(metadata, NotGiven) and not isinstance(metadata, dict) ): - raise TypeError("metadata must be a dictionary") + if isinstance(metadata, BaseModel): + metadata = metadata.model_dump() + else: + metadata = {} model = kwargs.get("model", None) or None @@ -375,10 +430,12 @@ def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> A if resource.type == "completion": prompt = kwargs.get("prompt", None) - elif resource.object == "Responses": - prompt = kwargs.get("input", None) + elif resource.object == "Responses" or resource.object == "AsyncResponses": + prompt = _extract_responses_prompt(kwargs) elif resource.type == "chat": prompt = _extract_chat_prompt(kwargs) + elif resource.type == "embedding": + prompt = kwargs.get("input", None) parsed_temperature = ( kwargs.get("temperature", 1) @@ -392,6 +449,12 @@ def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> A else float("inf") ) + parsed_max_completion_tokens = ( + kwargs.get("max_completion_tokens", None) + if not isinstance(kwargs.get("max_completion_tokens", float("inf")), NotGiven) + else None + ) + parsed_top_p = ( kwargs.get("top_p", 1) if not isinstance(kwargs.get("top_p", 1), NotGiven) @@ -418,18 +481,41 @@ def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> A parsed_n = kwargs.get("n", 1) if not isinstance(kwargs.get("n", 1), NotGiven) else 1 - modelParameters = { - "temperature": parsed_temperature, - "max_tokens": parsed_max_tokens, # casing? - "top_p": parsed_top_p, - "frequency_penalty": parsed_frequency_penalty, - "presence_penalty": parsed_presence_penalty, - } - if parsed_n is not None and parsed_n > 1: - modelParameters["n"] = parsed_n + if resource.type == "embedding": + parsed_dimensions = ( + kwargs.get("dimensions", None) + if not isinstance(kwargs.get("dimensions", None), NotGiven) + else None + ) + parsed_encoding_format = ( + kwargs.get("encoding_format", "float") + if not isinstance(kwargs.get("encoding_format", "float"), NotGiven) + else "float" + ) + + modelParameters = {} + if parsed_dimensions is not None: + modelParameters["dimensions"] = parsed_dimensions + if parsed_encoding_format != "float": + modelParameters["encoding_format"] = parsed_encoding_format + else: + modelParameters = { + "temperature": parsed_temperature, + "max_tokens": parsed_max_tokens, + "top_p": parsed_top_p, + "frequency_penalty": parsed_frequency_penalty, + "presence_penalty": parsed_presence_penalty, + } + + if parsed_max_completion_tokens is not None: + modelParameters.pop("max_tokens", None) + modelParameters["max_completion_tokens"] = parsed_max_completion_tokens + + if parsed_n is not None and isinstance(parsed_n, int) and parsed_n > 1: + modelParameters["n"] = parsed_n - if parsed_seed is not None: - modelParameters["seed"] = parsed_seed + if parsed_seed is not None: + modelParameters["seed"] = parsed_seed langfuse_prompt = kwargs.get("langfuse_prompt", None) @@ -467,6 +553,7 @@ def _create_langfuse_update( if usage is not None: update["usage_details"] = _parse_usage(usage) + update["cost_details"] = _parse_cost(usage) generation.update(**update) @@ -480,8 +567,8 @@ def _parse_usage(usage: Optional[Any] = None) -> Any: for tokens_details in [ "prompt_tokens_details", "completion_tokens_details", - "input_token_details", - "output_token_details", + "input_tokens_details", + "output_tokens_details", ]: if tokens_details in usage_dict and usage_dict[tokens_details] is not None: tokens_details_dict = ( @@ -493,9 +580,29 @@ def _parse_usage(usage: Optional[Any] = None) -> Any: k: v for k, v in tokens_details_dict.items() if v is not None } + if ( + len(usage_dict) == 2 + and "prompt_tokens" in usage_dict + and "total_tokens" in usage_dict + ): + # handle embedding usage + return {"input": usage_dict["prompt_tokens"]} + return usage_dict +def _parse_cost(usage: Optional[Any] = None) -> Any: + if usage is None: + return + + # OpenRouter is returning total cost of the invocation + # https://openrouter.ai/docs/use-cases/usage-accounting#cost-breakdown + if hasattr(usage, "cost") and isinstance(getattr(usage, "cost"), float): + return {"total": getattr(usage, "cost")} + + return None + + def _extract_streamed_response_api_response(chunks: Any) -> Any: completion, model, usage = None, None, None metadata = {} @@ -503,7 +610,8 @@ def _extract_streamed_response_api_response(chunks: Any) -> Any: for raw_chunk in chunks: chunk = raw_chunk.__dict__ if raw_response := chunk.get("response", None): - usage = chunk.get("usage", None) + usage = chunk.get("usage", None) or getattr(raw_response, "usage", None) + response = raw_response.__dict__ model = response.get("model") @@ -525,14 +633,16 @@ def _extract_streamed_response_api_response(chunks: Any) -> Any: def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any: completion: Any = defaultdict(lambda: None) if resource.type == "chat" else "" - model, usage = None, None + model, usage, finish_reason = None, None, None for chunk in chunks: if _is_openai_v1(): chunk = chunk.__dict__ model = model or chunk.get("model", None) or None - usage = chunk.get("usage", None) + chunk_usage = chunk.get("usage", None) + if chunk_usage is not None: + usage = chunk_usage choices = chunk.get("choices", []) @@ -541,10 +651,16 @@ def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any: choice = choice.__dict__ if resource.type == "chat": delta = choice.get("delta", None) + choice_finish_reason = choice.get("finish_reason", None) + if choice_finish_reason is not None: + finish_reason = choice_finish_reason - if _is_openai_v1(): + if _is_openai_v1() and delta is not None: delta = delta.__dict__ + if delta is None: + delta = {} + if delta.get("role", None) is not None: completion["role"] = delta["role"] @@ -570,7 +686,10 @@ def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any: ) curr["arguments"] += getattr(tool_call_chunk, "arguments", "") - elif delta.get("tool_calls", None) is not None and len(delta.get("tool_calls")) > 0: + elif ( + delta.get("tool_calls", None) is not None + and len(delta.get("tool_calls")) > 0 + ): curr = completion["tool_calls"] tool_call_chunk = getattr( delta.get("tool_calls", None)[0], "function", None @@ -598,8 +717,12 @@ def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any: curr[-1]["name"] = curr[-1]["name"] or getattr( tool_call_chunk, "name", None ) + + if curr[-1]["arguments"] is None: + curr[-1]["arguments"] = "" + curr[-1]["arguments"] += getattr( - tool_call_chunk, "arguments", None + tool_call_chunk, "arguments", "" ) if resource.type == "completion": @@ -632,7 +755,7 @@ def get_response_for_chat() -> Any: model, get_response_for_chat() if resource.type == "chat" else completion, usage, - None, + {"finish_reason": finish_reason} if finish_reason is not None else None, ) @@ -653,7 +776,7 @@ def _get_langfuse_data_from_default_response( completion = choice.text if _is_openai_v1() else choice.get("text", None) - elif resource.object == "Responses": + elif resource.object == "Responses" or resource.object == "AsyncResponses": output = response.get("output", {}) if not isinstance(output, list): @@ -682,6 +805,20 @@ def _get_langfuse_data_from_default_response( else choice.get("message", None) ) + elif resource.type == "embedding": + data = response.get("data", []) + if len(data) > 0: + first_embedding = data[0] + embedding_vector = ( + first_embedding.embedding + if hasattr(first_embedding, "embedding") + else first_embedding.get("embedding", []) + ) + completion = { + "dimensions": len(embedding_vector) if embedding_vector else 0, + "count": len(data), + } + usage = _parse_usage(response.get("usage", None)) return (model, completion, usage) @@ -700,6 +837,191 @@ def _is_streaming_response(response: Any) -> bool: ) +_openai_stream_iter_hook_installed = False + + +def _install_openai_stream_iteration_hooks() -> None: + global _openai_stream_iter_hook_installed + + if not _is_openai_v1(): + return + + if not _openai_stream_iter_hook_installed: + original_iter = openai.Stream.__iter__ + original_aiter = openai.AsyncStream.__aiter__ + + def traced_iter(self: Any) -> Any: + try: + yield from original_iter(self) + finally: + finalize_once = getattr(self, "_langfuse_finalize_once", None) + if finalize_once is not None: + finalize_once() + + async def traced_aiter(self: Any) -> Any: + try: + async for item in original_aiter(self): + yield item + finally: + finalize_once = getattr(self, "_langfuse_finalize_once", None) + if finalize_once is not None: + await finalize_once() + + setattr(openai.Stream, "__iter__", traced_iter) + setattr(openai.AsyncStream, "__aiter__", traced_aiter) + _openai_stream_iter_hook_installed = True + + +def _finalize_stream_response( + *, + resource: OpenAiDefinition, + items: list[Any], + generation: LangfuseGeneration, + completion_start_time: Optional[datetime], +) -> None: + try: + model, completion, usage, metadata = ( + _extract_streamed_response_api_response(items) + if resource.object == "Responses" or resource.object == "AsyncResponses" + else _extract_streamed_openai_response(resource, items) + ) + + _create_langfuse_update( + completion, + generation, + completion_start_time, + model=model, + usage=usage, + metadata=metadata, + ) + except Exception: + pass + finally: + generation.end() + + +def _instrument_openai_stream( + *, + resource: OpenAiDefinition, + response: Any, + generation: LangfuseGeneration, +) -> Any: + if not hasattr(response, "_iterator"): + return LangfuseResponseGeneratorSync( + resource=resource, + response=response, + generation=generation, + ) + + items: list[Any] = [] + raw_iterator = response._iterator + completion_start_time: Optional[datetime] = None + is_finalized = False + close = response.close + + def finalize_once() -> None: + nonlocal is_finalized + if is_finalized: + return + + is_finalized = True + _finalize_stream_response( + resource=resource, + items=items, + generation=generation, + completion_start_time=completion_start_time, + ) + + response._langfuse_finalize_once = finalize_once # type: ignore[attr-defined] + + def traced_iterator() -> Any: + nonlocal completion_start_time + try: + for item in raw_iterator: + items.append(item) + + if completion_start_time is None: + completion_start_time = _get_timestamp() + + yield item + finally: + finalize_once() + + def traced_close() -> Any: + try: + return close() + finally: + finalize_once() + + response._iterator = traced_iterator() + response.close = traced_close + + return response + + +def _instrument_openai_async_stream( + *, + resource: OpenAiDefinition, + response: Any, + generation: LangfuseGeneration, +) -> Any: + if not hasattr(response, "_iterator"): + return LangfuseResponseGeneratorAsync( + resource=resource, + response=response, + generation=generation, + ) + + items: list[Any] = [] + raw_iterator = response._iterator + completion_start_time: Optional[datetime] = None + is_finalized = False + close = response.close + + async def finalize_once() -> None: + nonlocal is_finalized + if is_finalized: + return + + is_finalized = True + _finalize_stream_response( + resource=resource, + items=items, + generation=generation, + completion_start_time=completion_start_time, + ) + + response._langfuse_finalize_once = finalize_once # type: ignore[attr-defined] + + async def traced_iterator() -> Any: + nonlocal completion_start_time + try: + async for item in raw_iterator: + items.append(item) + + if completion_start_time is None: + completion_start_time = _get_timestamp() + + yield item + finally: + await finalize_once() + + async def traced_close() -> Any: + try: + return await close() + finally: + await finalize_once() + + async def traced_aclose() -> Any: + return await traced_close() + + response._iterator = traced_iterator() + response.close = traced_close + response.aclose = traced_aclose + + return response + + @_langfuse_wrapper def _wrap( open_ai_resource: OpenAiDefinition, wrapped: Any, args: Any, kwargs: Any @@ -710,7 +1032,12 @@ def _wrap( langfuse_data = _get_langfuse_data_from_kwargs(open_ai_resource, langfuse_args) langfuse_client = get_client(public_key=langfuse_args["langfuse_public_key"]) - generation = langfuse_client.start_generation( + observation_type = ( + "embedding" if open_ai_resource.type == "embedding" else "generation" + ) + + generation = langfuse_client.start_observation( + as_type=observation_type, # type: ignore name=langfuse_data["name"], input=langfuse_data.get("input", None), metadata=langfuse_data.get("metadata", None), @@ -728,7 +1055,13 @@ def _wrap( try: openai_response = wrapped(**arg_extractor.get_openai_args()) - if _is_streaming_response(openai_response): + if _is_openai_v1() and isinstance(openai_response, openai.Stream): + return _instrument_openai_stream( + resource=open_ai_resource, + response=openai_response, + generation=generation, + ) + elif _is_streaming_response(openai_response): return LangfuseResponseGeneratorSync( resource=open_ai_resource, response=openai_response, @@ -747,11 +1080,14 @@ def _wrap( model=model, output=completion, usage_details=usage, + cost_details=_parse_cost(openai_response.usage) + if hasattr(openai_response, "usage") + else None, ).end() return openai_response except Exception as ex: - log.warning(ex) + logger.warning(ex) model = kwargs.get("model", None) or None generation.update( status_message=str(ex), @@ -773,7 +1109,12 @@ async def _wrap_async( langfuse_data = _get_langfuse_data_from_kwargs(open_ai_resource, langfuse_args) langfuse_client = get_client(public_key=langfuse_args["langfuse_public_key"]) - generation = langfuse_client.start_generation( + observation_type = ( + "embedding" if open_ai_resource.type == "embedding" else "generation" + ) + + generation = langfuse_client.start_observation( + as_type=observation_type, # type: ignore name=langfuse_data["name"], input=langfuse_data.get("input", None), metadata=langfuse_data.get("metadata", None), @@ -791,7 +1132,13 @@ async def _wrap_async( try: openai_response = await wrapped(**arg_extractor.get_openai_args()) - if _is_streaming_response(openai_response): + if _is_openai_v1() and isinstance(openai_response, openai.AsyncStream): + return _instrument_openai_async_stream( + resource=open_ai_resource, + response=openai_response, + generation=generation, + ) + elif _is_streaming_response(openai_response): return LangfuseResponseGeneratorAsync( resource=open_ai_resource, response=openai_response, @@ -810,11 +1157,14 @@ async def _wrap_async( output=completion, usage=usage, # backward compat for all V2 self hosters usage_details=usage, + cost_details=_parse_cost(openai_response.usage) + if hasattr(openai_response, "usage") + else None, ).end() return openai_response except Exception as ex: - log.warning(ex) + logger.warning(ex) model = kwargs.get("model", None) or None generation.update( status_message=str(ex), @@ -848,6 +1198,7 @@ def register_tracing() -> None: register_tracing() +_install_openai_stream_iteration_hooks() class LangfuseResponseGeneratorSync: @@ -864,6 +1215,7 @@ def __init__( self.response = response self.generation = generation self.completion_start_time: Optional[datetime] = None + self._is_finalized = False def __iter__(self) -> Any: try: @@ -896,28 +1248,28 @@ def __enter__(self) -> Any: return self.__iter__() def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - pass + self.close() - def _finalize(self) -> None: - try: - model, completion, usage, metadata = ( - _extract_streamed_response_api_response(self.items) - if self.resource.object == "Responses" - else _extract_streamed_openai_response(self.resource, self.items) - ) + def close(self) -> None: + close = getattr(self.response, "close", None) - _create_langfuse_update( - completion, - self.generation, - self.completion_start_time, - model=model, - usage=usage, - metadata=metadata, - ) - except Exception: - pass + try: + if callable(close): + close() finally: - self.generation.end() + self._finalize() + + def _finalize(self) -> None: + if self._is_finalized: + return + + self._is_finalized = True + _finalize_stream_response( + resource=self.resource, + items=self.items, + generation=self.generation, + completion_start_time=self.completion_start_time, + ) class LangfuseResponseGeneratorAsync: @@ -934,6 +1286,7 @@ def __init__( self.response = response self.generation = generation self.completion_start_time: Optional[datetime] = None + self._is_finalized = False async def __aiter__(self) -> Any: try: @@ -966,39 +1319,56 @@ async def __aenter__(self) -> Any: return self.__aiter__() async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - pass + await self.aclose() async def _finalize(self) -> None: - try: - model, completion, usage, metadata = ( - _extract_streamed_response_api_response(self.items) - if self.resource.object == "Responses" - else _extract_streamed_openai_response(self.resource, self.items) - ) - - _create_langfuse_update( - completion, - self.generation, - self.completion_start_time, - model=model, - usage=usage, - metadata=metadata, - ) - except Exception: - pass - finally: - self.generation.end() + if self._is_finalized: + return + + self._is_finalized = True + _finalize_stream_response( + resource=self.resource, + items=self.items, + generation=self.generation, + completion_start_time=self.completion_start_time, + ) async def close(self) -> None: """Close the response and release the connection. Automatically called if the response body is read to completion. """ - await self.response.close() + close = getattr(self.response, "close", None) + aclose = getattr(self.response, "aclose", None) + + try: + if callable(close): + result = close() + if isawaitable(result): + await result + elif callable(aclose): + result = aclose() + if isawaitable(result): + await result + finally: + await self._finalize() async def aclose(self) -> None: """Close the response and release the connection. Automatically called if the response body is read to completion. """ - await self.response.aclose() + aclose = getattr(self.response, "aclose", None) + close = getattr(self.response, "close", None) + + try: + if callable(aclose): + result = aclose() + if isawaitable(result): + await result + elif callable(close): + result = close() + if isawaitable(result): + await result + finally: + await self._finalize() diff --git a/langfuse/span_filter.py b/langfuse/span_filter.py new file mode 100644 index 000000000..3c086b15e --- /dev/null +++ b/langfuse/span_filter.py @@ -0,0 +1,17 @@ +"""Public span filter helpers for Langfuse OpenTelemetry export control.""" + +from langfuse._client.span_filter import ( + KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES, + is_default_export_span, + is_genai_span, + is_known_llm_instrumentor, + is_langfuse_span, +) + +__all__ = [ + "is_default_export_span", + "is_langfuse_span", + "is_genai_span", + "is_known_llm_instrumentor", + "KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES", +] diff --git a/langfuse/types.py b/langfuse/types.py index b654fffed..c3029e713 100644 --- a/langfuse/types.py +++ b/langfuse/types.py @@ -1,15 +1,28 @@ -"""@private""" +"""Public API for all Langfuse types. + +This module provides a centralized location for importing commonly used types +from the Langfuse SDK, making them easily accessible without requiring nested imports. + +Example: + ```python + from langfuse.types import Evaluation, LocalExperimentItem, TaskFunction + + # Define your task function + def my_task(*, item: LocalExperimentItem, **kwargs) -> str: + return f"Processed: {item['input']}" + + # Define your evaluator + def my_evaluator(*, output: str, **kwargs) -> Evaluation: + return {"name": "length", "value": len(output)} + ``` +""" -from datetime import datetime from typing import ( Any, Dict, - List, Literal, - Optional, Protocol, TypedDict, - Union, ) try: @@ -17,41 +30,15 @@ except ImportError: from typing_extensions import NotRequired -from pydantic import BaseModel -from langfuse.api import MediaContentType, UsageDetails -from langfuse.model import MapValue, ModelUsage, PromptClient +from langfuse.api import MediaContentType SpanLevel = Literal["DEBUG", "DEFAULT", "WARNING", "ERROR"] -ScoreDataType = Literal["NUMERIC", "CATEGORICAL", "BOOLEAN"] - +ScoreDataType = Literal["NUMERIC", "CATEGORICAL", "BOOLEAN", "TEXT"] -class TraceMetadata(TypedDict): - name: Optional[str] - user_id: Optional[str] - session_id: Optional[str] - version: Optional[str] - release: Optional[str] - metadata: Optional[Any] - tags: Optional[List[str]] - public: Optional[bool] - - -class ObservationParams(TraceMetadata, TypedDict): - input: Optional[Any] - output: Optional[Any] - level: Optional[SpanLevel] - status_message: Optional[str] - start_time: Optional[datetime] - end_time: Optional[datetime] - completion_start_time: Optional[datetime] - model: Optional[str] - model_parameters: Optional[Dict[str, MapValue]] - usage: Optional[Union[BaseModel, ModelUsage]] - usage_details: Optional[UsageDetails] - cost_details: Optional[Dict[str, float]] - prompt: Optional[PromptClient] +# Text scores are not supported for evals and experiments +ExperimentScoreType = Literal["NUMERIC", "CATEGORICAL", "BOOLEAN"] class MaskFunction(Protocol): @@ -84,3 +71,13 @@ class ParsedMediaReference(TypedDict): class TraceContext(TypedDict): trace_id: str parent_span_id: NotRequired[str] + + +__all__ = [ + "SpanLevel", + "ScoreDataType", + "ExperimentScoreType", + "MaskFunction", + "ParsedMediaReference", + "TraceContext", +] diff --git a/langfuse/version.py b/langfuse/version.py deleted file mode 100644 index 40a439362..000000000 --- a/langfuse/version.py +++ /dev/null @@ -1,3 +0,0 @@ -"""@private""" - -__version__ = "3.2.1" diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index fbc73e691..000000000 --- a/poetry.lock +++ /dev/null @@ -1,5613 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.12.14" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248"}, - {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb"}, - {file = "aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61"}, - {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8"}, - {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3"}, - {file = "aiohttp-3.12.14-cp310-cp310-win32.whl", hash = "sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c"}, - {file = "aiohttp-3.12.14-cp310-cp310-win_amd64.whl", hash = "sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393"}, - {file = "aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe"}, - {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0"}, - {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28"}, - {file = "aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b"}, - {file = "aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a"}, - {file = "aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660"}, - {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425"}, - {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0"}, - {file = "aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729"}, - {file = "aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e"}, - {file = "aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd"}, - {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3"}, - {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758"}, - {file = "aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5"}, - {file = "aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b8cc6b05e94d837bcd71c6531e2344e1ff0fb87abe4ad78a9261d67ef5d83eae"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1dcb015ac6a3b8facd3677597edd5ff39d11d937456702f0bb2b762e390a21b"}, - {file = "aiohttp-3.12.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3779ed96105cd70ee5e85ca4f457adbce3d9ff33ec3d0ebcdf6c5727f26b21b3"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:717a0680729b4ebd7569c1dcd718c46b09b360745fd8eb12317abc74b14d14d0"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b5dd3a2ef7c7e968dbbac8f5574ebeac4d2b813b247e8cec28174a2ba3627170"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4710f77598c0092239bc12c1fcc278a444e16c7032d91babf5abbf7166463f7b"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3e9f75ae842a6c22a195d4a127263dbf87cbab729829e0bd7857fb1672400b2"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9c8d55d6802086edd188e3a7d85a77787e50d56ce3eb4757a3205fa4657922"}, - {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79b29053ff3ad307880d94562cca80693c62062a098a5776ea8ef5ef4b28d140"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23e1332fff36bebd3183db0c7a547a1da9d3b4091509f6d818e098855f2f27d3"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a564188ce831fd110ea76bcc97085dd6c625b427db3f1dbb14ca4baa1447dcbc"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a7a1b4302f70bb3ec40ca86de82def532c97a80db49cac6a6700af0de41af5ee"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1b07ccef62950a2519f9bfc1e5b294de5dd84329f444ca0b329605ea787a3de5"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:938bd3ca6259e7e48b38d84f753d548bd863e0c222ed6ee6ace3fd6752768a84"}, - {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8bc784302b6b9f163b54c4e93d7a6f09563bd01ff2b841b29ed3ac126e5040bf"}, - {file = "aiohttp-3.12.14-cp39-cp39-win32.whl", hash = "sha256:a3416f95961dd7d5393ecff99e3f41dc990fb72eda86c11f2a60308ac6dcd7a0"}, - {file = "aiohttp-3.12.14-cp39-cp39-win_amd64.whl", hash = "sha256:196858b8820d7f60578f8b47e5669b3195c21d8ab261e39b1d705346458f445f"}, - {file = "aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.4.0" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns (>=3.3.0)", "brotlicffi"] - -[[package]] -name = "aiosignal" -version = "1.4.0" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, - {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} - -[[package]] -name = "aiosqlite" -version = "0.21.0" -description = "asyncio bridge to the standard sqlite3 module" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"}, - {file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"}, -] - -[package.dependencies] -typing_extensions = ">=4.0" - -[package.extras] -dev = ["attribution (==1.7.1)", "black (==24.3.0)", "build (>=1.2)", "coverage[toml] (==7.6.10)", "flake8 (==7.0.0)", "flake8-bugbear (==24.12.12)", "flit (==3.10.1)", "mypy (==1.14.1)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"] -docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"] - -[[package]] -name = "annotated-types" -version = "0.6.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - -[[package]] -name = "anthropic" -version = "0.39.0" -description = "The official Python library for the anthropic API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anthropic-0.39.0-py3-none-any.whl", hash = "sha256:ea17093ae0ce0e1768b0c46501d6086b5bcd74ff39d68cd2d6396374e9de7c09"}, - {file = "anthropic-0.39.0.tar.gz", hash = "sha256:94671cc80765f9ce693f76d63a97ee9bef4c2d6063c044e983d21a2e262f63ba"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -boto3 = {version = ">=1.28.57", optional = true, markers = "extra == \"bedrock\""} -botocore = {version = ">=1.31.57", optional = true, markers = "extra == \"bedrock\""} -distro = ">=1.7.0,<2" -google-auth = {version = ">=2,<3", optional = true, markers = "extra == \"vertex\""} -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -typing-extensions = ">=4.7,<5" - -[package.extras] -bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] -vertex = ["google-auth (>=2,<3)"] - -[[package]] -name = "anyio" -version = "4.4.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "asgiref" -version = "3.8.1" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.8" -files = [ - {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, - {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] - -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - -[[package]] -name = "backoff" -version = "2.2.1" -description = "Function decoration for backoff and retry" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, - {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, -] - -[[package]] -name = "banks" -version = "2.1.3" -description = "A prompt programming language" -optional = false -python-versions = ">=3.9" -files = [ - {file = "banks-2.1.3-py3-none-any.whl", hash = "sha256:9e1217dc977e6dd1ce42c5ff48e9bcaf238d788c81b42deb6a555615ffcffbab"}, - {file = "banks-2.1.3.tar.gz", hash = "sha256:c0dd2cb0c5487274a513a552827e6a8ddbd0ab1a1b967f177e71a6e4748a3ed2"}, -] - -[package.dependencies] -deprecated = "*" -eval-type-backport = {version = "*", markers = "python_version < \"3.10\""} -griffe = "*" -jinja2 = "*" -platformdirs = "*" -pydantic = "*" - -[package.extras] -all = ["litellm", "redis"] - -[[package]] -name = "bcrypt" -version = "4.1.2" -description = "Modern password hashing for your software and your servers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, - {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, - {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, - {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, - {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, - {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, - {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, - {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, - {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, - {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, - {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, - {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, - {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, - {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, - {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, - {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, - {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, - {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, - {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, - {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, - {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, - {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, - {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, - {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, - {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, - {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, - {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, -] - -[package.extras] -tests = ["pytest (>=3.2.1,!=3.3.0)"] -typecheck = ["mypy"] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "boto3" -version = "1.35.77" -description = "The AWS SDK for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "boto3-1.35.77-py3-none-any.whl", hash = "sha256:a09871805f8e462349a1c33c23eb413668df0bf68424e61d53518e1a7d883b2f"}, - {file = "boto3-1.35.77.tar.gz", hash = "sha256:cc819cdbccbc2d0dc185f1dcfe74cf3809489c4cae63c2e5d6a557aa0c5ab928"}, -] - -[package.dependencies] -botocore = ">=1.35.77,<1.36.0" -jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.10.0,<0.11.0" - -[package.extras] -crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] - -[[package]] -name = "botocore" -version = "1.35.77" -description = "Low-level, data-driven core of boto 3." -optional = false -python-versions = ">=3.8" -files = [ - {file = "botocore-1.35.77-py3-none-any.whl", hash = "sha256:3faa27d65841499762228902d7e215fa99a4c2fdc76c9113e1c3f339bdf685b8"}, - {file = "botocore-1.35.77.tar.gz", hash = "sha256:17b778016644e9342ca3ff2f430c1d1db0c6126e9b41a57cff52ac58e7a455e0"}, -] - -[package.dependencies] -jmespath = ">=0.7.1,<2.0.0" -python-dateutil = ">=2.1,<3.0.0" -urllib3 = [ - {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, - {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, -] - -[package.extras] -crt = ["awscrt (==0.22.0)"] - -[[package]] -name = "bs4" -version = "0.0.2" -description = "Dummy package for Beautiful Soup (beautifulsoup4)" -optional = false -python-versions = "*" -files = [ - {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, - {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, -] - -[package.dependencies] -beautifulsoup4 = "*" - -[[package]] -name = "bson" -version = "0.5.10" -description = "BSON codec for Python" -optional = false -python-versions = "*" -files = [ - {file = "bson-0.5.10.tar.gz", hash = "sha256:d6511b2ab051139a9123c184de1a04227262173ad593429d21e443d6462d6590"}, -] - -[package.dependencies] -python-dateutil = ">=2.4.0" -six = ">=1.9.0" - -[[package]] -name = "build" -version = "1.2.1" -description = "A simple, correct Python build frontend" -optional = false -python-versions = ">=3.8" -files = [ - {file = "build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4"}, - {file = "build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "os_name == \"nt\""} -importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} -packaging = ">=19.1" -pyproject_hooks = "*" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] -test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] -typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] -uv = ["uv (>=0.1.18)"] -virtualenv = ["virtualenv (>=20.0.35)"] - -[[package]] -name = "cachetools" -version = "5.3.3" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, -] - -[[package]] -name = "certifi" -version = "2024.7.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "chroma-hnswlib" -version = "0.7.6" -description = "Chromas fork of hnswlib" -optional = false -python-versions = "*" -files = [ - {file = "chroma_hnswlib-0.7.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f35192fbbeadc8c0633f0a69c3d3e9f1a4eab3a46b65458bbcbcabdd9e895c36"}, - {file = "chroma_hnswlib-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f007b608c96362b8f0c8b6b2ac94f67f83fcbabd857c378ae82007ec92f4d82"}, - {file = "chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:456fd88fa0d14e6b385358515aef69fc89b3c2191706fd9aee62087b62aad09c"}, - {file = "chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dfaae825499c2beaa3b75a12d7ec713b64226df72a5c4097203e3ed532680da"}, - {file = "chroma_hnswlib-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:2487201982241fb1581be26524145092c95902cb09fc2646ccfbc407de3328ec"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81181d54a2b1e4727369486a631f977ffc53c5533d26e3d366dda243fb0998ca"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b4ab4e11f1083dd0a11ee4f0e0b183ca9f0f2ed63ededba1935b13ce2b3606f"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53db45cd9173d95b4b0bdccb4dbff4c54a42b51420599c32267f3abbeb795170"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c093f07a010b499c00a15bc9376036ee4800d335360570b14f7fe92badcdcf9"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:0540b0ac96e47d0aa39e88ea4714358ae05d64bbe6bf33c52f316c664190a6a3"}, - {file = "chroma_hnswlib-0.7.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e87e9b616c281bfbe748d01705817c71211613c3b063021f7ed5e47173556cb7"}, - {file = "chroma_hnswlib-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec5ca25bc7b66d2ecbf14502b5729cde25f70945d22f2aaf523c2d747ea68912"}, - {file = "chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305ae491de9d5f3c51e8bd52d84fdf2545a4a2bc7af49765cda286b7bb30b1d4"}, - {file = "chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:822ede968d25a2c88823ca078a58f92c9b5c4142e38c7c8b4c48178894a0a3c5"}, - {file = "chroma_hnswlib-0.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2fe6ea949047beed19a94b33f41fe882a691e58b70c55fdaa90274ae78be046f"}, - {file = "chroma_hnswlib-0.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feceff971e2a2728c9ddd862a9dd6eb9f638377ad98438876c9aeac96c9482f5"}, - {file = "chroma_hnswlib-0.7.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb0633b60e00a2b92314d0bf5bbc0da3d3320be72c7e3f4a9b19f4609dc2b2ab"}, - {file = "chroma_hnswlib-0.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:a566abe32fab42291f766d667bdbfa234a7f457dcbd2ba19948b7a978c8ca624"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6be47853d9a58dedcfa90fc846af202b071f028bbafe1d8711bf64fe5a7f6111"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a7af35bdd39a88bffa49f9bb4bf4f9040b684514a024435a1ef5cdff980579d"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a53b1f1551f2b5ad94eb610207bde1bb476245fc5097a2bec2b476c653c58bde"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3085402958dbdc9ff5626ae58d696948e715aef88c86d1e3f9285a88f1afd3bc"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:77326f658a15adfb806a16543f7db7c45f06fd787d699e643642d6bde8ed49c4"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93b056ab4e25adab861dfef21e1d2a2756b18be5bc9c292aa252fa12bb44e6ae"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe91f018b30452c16c811fd6c8ede01f84e5a9f3c23e0758775e57f1c3778871"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6c0e627476f0f4d9e153420d36042dd9c6c3671cfd1fe511c0253e38c2a1039"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e9796a4536b7de6c6d76a792ba03e08f5aaa53e97e052709568e50b4d20c04f"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:d30e2db08e7ffdcc415bd072883a322de5995eb6ec28a8f8c054103bbd3ec1e0"}, - {file = "chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7"}, -] - -[package.dependencies] -numpy = "*" - -[[package]] -name = "chromadb" -version = "0.5.5" -description = "Chroma." -optional = false -python-versions = ">=3.8" -files = [ - {file = "chromadb-0.5.5-py3-none-any.whl", hash = "sha256:2a5a4b84cb0fc32b380e193be68cdbadf3d9f77dbbf141649be9886e42910ddd"}, - {file = "chromadb-0.5.5.tar.gz", hash = "sha256:84f4bfee320fb4912cbeb4d738f01690891e9894f0ba81f39ee02867102a1c4d"}, -] - -[package.dependencies] -bcrypt = ">=4.0.1" -build = ">=1.0.3" -chroma-hnswlib = "0.7.6" -fastapi = ">=0.95.2" -grpcio = ">=1.58.0" -httpx = ">=0.27.0" -importlib-resources = "*" -kubernetes = ">=28.1.0" -mmh3 = ">=4.0.1" -numpy = ">=1.22.5,<2.0.0" -onnxruntime = ">=1.14.1" -opentelemetry-api = ">=1.2.0" -opentelemetry-exporter-otlp-proto-grpc = ">=1.2.0" -opentelemetry-instrumentation-fastapi = ">=0.41b0" -opentelemetry-sdk = ">=1.2.0" -orjson = ">=3.9.12" -overrides = ">=7.3.1" -posthog = ">=2.4.0" -pydantic = ">=1.9" -pypika = ">=0.48.9" -PyYAML = ">=6.0.0" -tenacity = ">=8.2.3" -tokenizers = ">=0.13.2" -tqdm = ">=4.65.0" -typer = ">=0.9.0" -typing-extensions = ">=4.5.0" -uvicorn = {version = ">=0.18.3", extras = ["standard"]} - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "cohere" -version = "5.8.1" -description = "" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "cohere-5.8.1-py3-none-any.whl", hash = "sha256:92362c651dfbfef8c5d34e95de394578d7197ed7875c6fcbf101e84b60db7fbd"}, - {file = "cohere-5.8.1.tar.gz", hash = "sha256:4c0c4468f15f9ad7fb7af15cc9f7305cd6df51243d69e203682be87e9efa5071"}, -] - -[package.dependencies] -boto3 = ">=1.34.0,<2.0.0" -fastavro = ">=1.9.4,<2.0.0" -httpx = ">=0.21.2" -httpx-sse = "0.4.0" -parameterized = ">=0.9.0,<0.10.0" -pydantic = ">=1.9.2" -pydantic-core = ">=2.18.2,<3.0.0" -requests = ">=2.0.0,<3.0.0" -tokenizers = ">=0.15,<1" -types-requests = ">=2.0.0,<3.0.0" -typing_extensions = ">=4.0.0" - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coloredlogs" -version = "15.0.1" -description = "Colored terminal output for Python's logging module" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, - {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, -] - -[package.dependencies] -humanfriendly = ">=9.1" - -[package.extras] -cron = ["capturer (>=2.4)"] - -[[package]] -name = "dashscope" -version = "1.20.3" -description = "dashscope client sdk library" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "dashscope-1.20.3-py3-none-any.whl", hash = "sha256:8dd4161b8387f1220d38c7a78fccf4a0491d8518949a91ec3e0834c45f46aa78"}, -] - -[package.dependencies] -aiohttp = "*" -requests = "*" -websocket-client = "*" - -[package.extras] -tokenizer = ["tiktoken"] - -[[package]] -name = "dataclasses-json" -version = "0.6.5" -description = "Easily serialize dataclasses to and from JSON." -optional = false -python-versions = "<4.0,>=3.7" -files = [ - {file = "dataclasses_json-0.6.5-py3-none-any.whl", hash = "sha256:f49c77aa3a85cac5bf5b7f65f4790ca0d2be8ef4d92c75e91ba0103072788a39"}, - {file = "dataclasses_json-0.6.5.tar.gz", hash = "sha256:1c287594d9fcea72dc42d6d3836cf14848c2dc5ce88f65ed61b36b57f515fe26"}, -] - -[package.dependencies] -marshmallow = ">=3.18.0,<4.0.0" -typing-inspect = ">=0.4.0,<1" - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - -[[package]] -name = "deprecated" -version = "1.2.14" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] - -[[package]] -name = "dirtyjson" -version = "1.0.8" -description = "JSON decoder for Python that can extract data from the muck" -optional = false -python-versions = "*" -files = [ - {file = "dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53"}, - {file = "dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd"}, -] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "dnspython" -version = "2.6.1" -description = "DNS toolkit" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, - {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, -] - -[package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=41)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=0.9.25)"] -idna = ["idna (>=3.6)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - -[[package]] -name = "docstring-parser" -version = "0.16" -description = "Parse Python docstrings in reST, Google and Numpydoc format" -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637"}, - {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, -] - -[[package]] -name = "eval-type-backport" -version = "0.2.0" -description = "Like `typing._eval_type`, but lets older Python versions use newer typing features." -optional = false -python-versions = ">=3.8" -files = [ - {file = "eval_type_backport-0.2.0-py3-none-any.whl", hash = "sha256:ac2f73d30d40c5a30a80b8739a789d6bb5e49fdffa66d7912667e2015d9c9933"}, - {file = "eval_type_backport-0.2.0.tar.gz", hash = "sha256:68796cfbc7371ebf923f03bdf7bef415f3ec098aeced24e054b253a0e78f7b37"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "exceptiongroup" -version = "1.2.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.1.1" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "fastapi" -version = "0.115.2" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fastapi-0.115.2-py3-none-any.whl", hash = "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86"}, - {file = "fastapi-0.115.2.tar.gz", hash = "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.37.2,<0.41.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "fastavro" -version = "1.9.4" -description = "Fast read/write of AVRO files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fastavro-1.9.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:60cb38f07462a7fb4e4440ed0de67d3d400ae6b3d780f81327bebde9aa55faef"}, - {file = "fastavro-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:063d01d197fc929c20adc09ca9f0ca86d33ac25ee0963ce0b438244eee8315ae"}, - {file = "fastavro-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a9053fcfbc895f2a16a4303af22077e3a8fdcf1cd5d6ed47ff2ef22cbba2f0"}, - {file = "fastavro-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:02bf1276b7326397314adf41b34a4890f6ffa59cf7e0eb20b9e4ab0a143a1598"}, - {file = "fastavro-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56bed9eca435389a8861e6e2d631ec7f8f5dda5b23f93517ac710665bd34ca29"}, - {file = "fastavro-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:0cd2099c8c672b853e0b20c13e9b62a69d3fbf67ee7c59c7271ba5df1680310d"}, - {file = "fastavro-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:af8c6d8c43a02b5569c093fc5467469541ac408c79c36a5b0900d3dd0b3ba838"}, - {file = "fastavro-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a138710bd61580324d23bc5e3df01f0b82aee0a76404d5dddae73d9e4c723f"}, - {file = "fastavro-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:903d97418120ca6b6a7f38a731166c1ccc2c4344ee5e0470d09eb1dc3687540a"}, - {file = "fastavro-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c443eeb99899d062dbf78c525e4614dd77e041a7688fa2710c224f4033f193ae"}, - {file = "fastavro-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ac26ab0774d1b2b7af6d8f4300ad20bbc4b5469e658a02931ad13ce23635152f"}, - {file = "fastavro-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:cf7247874c22be856ba7d1f46a0f6e0379a6025f1a48a7da640444cbac6f570b"}, - {file = "fastavro-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:68912f2020e1b3d70557260b27dd85fb49a4fc6bfab18d384926127452c1da4c"}, - {file = "fastavro-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6925ce137cdd78e109abdb0bc33aad55de6c9f2d2d3036b65453128f2f5f5b92"}, - {file = "fastavro-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b928cd294e36e35516d0deb9e104b45be922ba06940794260a4e5dbed6c192a"}, - {file = "fastavro-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:90c9838bc4c991ffff5dd9d88a0cc0030f938b3fdf038cdf6babde144b920246"}, - {file = "fastavro-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:eca6e54da571b06a3c5a72dbb7212073f56c92a6fbfbf847b91c347510f8a426"}, - {file = "fastavro-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4b02839ac261100cefca2e2ad04cdfedc556cb66b5ec735e0db428e74b399de"}, - {file = "fastavro-1.9.4-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4451ee9a305a73313a1558d471299f3130e4ecc10a88bf5742aa03fb37e042e6"}, - {file = "fastavro-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8524fccfb379565568c045d29b2ebf71e1f2c0dd484aeda9fe784ef5febe1a8"}, - {file = "fastavro-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33d0a00a6e09baa20f6f038d7a2ddcb7eef0e7a9980e947a018300cb047091b8"}, - {file = "fastavro-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:23d7e5b29c9bf6f26e8be754b2c8b919838e506f78ef724de7d22881696712fc"}, - {file = "fastavro-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e6ab3ee53944326460edf1125b2ad5be2fadd80f7211b13c45fa0c503b4cf8d"}, - {file = "fastavro-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:64d335ec2004204c501f8697c385d0a8f6b521ac82d5b30696f789ff5bc85f3c"}, - {file = "fastavro-1.9.4-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:7e05f44c493e89e73833bd3ff3790538726906d2856f59adc8103539f4a1b232"}, - {file = "fastavro-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:253c63993250bff4ee7b11fb46cf3a4622180a783bedc82a24c6fdcd1b10ca2a"}, - {file = "fastavro-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d6942eb1db14640c2581e0ecd1bbe0afc8a83731fcd3064ae7f429d7880cb7"}, - {file = "fastavro-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d47bb66be6091cd48cfe026adcad11c8b11d7d815a2949a1e4ccf03df981ca65"}, - {file = "fastavro-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c293897f12f910e58a1024f9c77f565aa8e23b36aafda6ad8e7041accc57a57f"}, - {file = "fastavro-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:f05d2afcb10a92e2a9e580a3891f090589b3e567fdc5641f8a46a0b084f120c3"}, - {file = "fastavro-1.9.4.tar.gz", hash = "sha256:56b8363e360a1256c94562393dc7f8611f3baf2b3159f64fb2b9c6b87b14e876"}, -] - -[package.extras] -codecs = ["cramjam", "lz4", "zstandard"] -lz4 = ["lz4"] -snappy = ["cramjam"] -zstandard = ["zstandard"] - -[[package]] -name = "filelock" -version = "3.14.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] - -[[package]] -name = "filetype" -version = "1.2.0" -description = "Infer file type and MIME type of any file/buffer. No external dependencies." -optional = false -python-versions = "*" -files = [ - {file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"}, - {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, -] - -[[package]] -name = "flatbuffers" -version = "24.3.25" -description = "The FlatBuffers serialization format for Python" -optional = false -python-versions = "*" -files = [ - {file = "flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812"}, - {file = "flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4"}, -] - -[[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, -] - -[[package]] -name = "fsspec" -version = "2024.3.1" -description = "File-system specification" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fsspec-2024.3.1-py3-none-any.whl", hash = "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512"}, - {file = "fsspec-2024.3.1.tar.gz", hash = "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -devel = ["pytest", "pytest-cov"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -tqdm = ["tqdm"] - -[[package]] -name = "google-api-core" -version = "2.24.2" -description = "Google API client core library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, - {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, -] - -[package.dependencies] -google-auth = ">=2.14.1,<3.0.0" -googleapis-common-protos = ">=1.56.2,<2.0.0" -grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, - {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] -grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, - {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] -proto-plus = [ - {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, - {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, -] -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" -requests = ">=2.18.0,<3.0.0" - -[package.extras] -async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] - -[[package]] -name = "google-auth" -version = "2.29.0" -description = "Google Authentication Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-auth-2.29.0.tar.gz", hash = "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360"}, - {file = "google_auth-2.29.0-py2.py3-none-any.whl", hash = "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415"}, -] - -[package.dependencies] -cachetools = ">=2.0.0,<6.0" -pyasn1-modules = ">=0.2.1" -rsa = ">=3.1.4,<5" - -[package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] -enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] -pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] -reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0.dev0)"] - -[[package]] -name = "google-cloud-aiplatform" -version = "1.73.0" -description = "Vertex AI API client library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "google_cloud_aiplatform-1.73.0-py2.py3-none-any.whl", hash = "sha256:6f9aebc1cb2277048093f17214c5f4ec9129fa347b8b22d784f780b12b8865a9"}, - {file = "google_cloud_aiplatform-1.73.0.tar.gz", hash = "sha256:687d4d6dd26439db42d38b835ea0da7ebb75c20ca8e17666669536b253637e74"}, -] - -[package.dependencies] -docstring-parser = "<1" -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.8.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<3.0.0dev" -google-cloud-bigquery = ">=1.15.0,<3.20.0 || >3.20.0,<4.0.0dev" -google-cloud-resource-manager = ">=1.3.3,<3.0.0dev" -google-cloud-storage = ">=1.32.0,<3.0.0dev" -packaging = ">=14.3" -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" -pydantic = "<3" -shapely = "<3.0.0dev" - -[package.extras] -autologging = ["mlflow (>=1.27.0,<=2.16.0)"] -cloud-profiler = ["tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (>=2.4.0,<3.0.0dev)", "werkzeug (>=2.0.0,<2.1.0dev)"] -datasets = ["pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)"] -endpoint = ["requests (>=2.28.1)"] -evaluation = ["pandas (>=1.0.0)", "tqdm (>=4.23.0)"] -full = ["docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<=0.114.0)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-vizier (>=0.1.6)", "httpx (>=0.23.0,<0.25.0)", "immutabledict", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.16.0)", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "requests (>=2.28.1)", "setuptools (<70.0.0)", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)"] -langchain = ["langchain (>=0.1.16,<0.4)", "langchain-core (<0.4)", "langchain-google-vertexai (<3)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)"] -langchain-testing = ["absl-py", "cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "langchain (>=0.1.16,<0.4)", "langchain-core (<0.4)", "langchain-google-vertexai (<3)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.6.3,<3)", "pytest-xdist"] -lit = ["explainable-ai-sdk (>=1.0.0)", "lit-nlp (==0.4.0)", "pandas (>=1.0.0)", "tensorflow (>=2.3.0,<3.0.0dev)"] -metadata = ["numpy (>=1.15.0)", "pandas (>=1.0.0)"] -pipelines = ["pyyaml (>=5.3.1,<7)"] -prediction = ["docker (>=5.0.3)", "fastapi (>=0.71.0,<=0.114.0)", "httpx (>=0.23.0,<0.25.0)", "starlette (>=0.17.1)", "uvicorn[standard] (>=0.16.0)"] -private-endpoints = ["requests (>=2.28.1)", "urllib3 (>=1.21.1,<1.27)"] -ray = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0)", "pyarrow (>=6.0.1)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "setuptools (<70.0.0)"] -ray-testing = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0)", "pyarrow (>=6.0.1)", "pytest-xdist", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "ray[train]", "scikit-learn", "setuptools (<70.0.0)", "tensorflow", "torch (>=2.0.0,<2.1.0)", "xgboost", "xgboost-ray"] -reasoningengine = ["cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.6.3,<3)"] -tensorboard = ["tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "werkzeug (>=2.0.0,<2.1.0dev)"] -testing = ["aiohttp", "bigframes", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<=0.114.0)", "google-api-core (>=2.11,<3.0.0)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-vizier (>=0.1.6)", "grpcio-testing", "httpx (>=0.23.0,<0.25.0)", "immutabledict", "ipython", "kfp (>=2.6.0,<3.0.0)", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.16.0)", "nltk", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pytest-asyncio", "pytest-xdist", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "requests (>=2.28.1)", "requests-toolbelt (<1.0.0)", "scikit-learn", "sentencepiece (>=0.2.0)", "setuptools (<70.0.0)", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (==2.13.0)", "tensorflow (==2.16.1)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "torch (>=2.0.0,<2.1.0)", "torch (>=2.2.0)", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)", "xgboost"] -tokenization = ["sentencepiece (>=0.2.0)"] -vizier = ["google-vizier (>=0.1.6)"] -xai = ["tensorflow (>=2.3.0,<3.0.0dev)"] - -[[package]] -name = "google-cloud-bigquery" -version = "3.21.0" -description = "Google BigQuery API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-cloud-bigquery-3.21.0.tar.gz", hash = "sha256:6265c39f9d5bdf50f11cb81a9c2a0605d285df34ac139de0d2333b1250add0ff"}, - {file = "google_cloud_bigquery-3.21.0-py2.py3-none-any.whl", hash = "sha256:83a090aae16b3a687ef22e7b0a1b551e18da615b1c4855c5f312f198959e7739"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<3.0.0dev" -google-cloud-core = ">=1.6.0,<3.0.0dev" -google-resumable-media = ">=0.6.0,<3.0dev" -packaging = ">=20.0.0" -python-dateutil = ">=2.7.2,<3.0dev" -requests = ">=2.21.0,<3.0.0dev" - -[package.extras] -all = ["Shapely (>=1.8.4,<3.0.0dev)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "importlib-metadata (>=1.0.0)", "ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "proto-plus (>=1.15.0,<2.0.0dev)", "protobuf (>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] -bigquery-v2 = ["proto-plus (>=1.15.0,<2.0.0dev)", "protobuf (>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev)"] -bqstorage = ["google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "pyarrow (>=3.0.0)"] -geopandas = ["Shapely (>=1.8.4,<3.0.0dev)", "geopandas (>=0.9.0,<1.0dev)"] -ipython = ["ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)"] -ipywidgets = ["ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)"] -opentelemetry = ["opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)"] -pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "importlib-metadata (>=1.0.0)", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] -tqdm = ["tqdm (>=4.7.4,<5.0.0dev)"] - -[[package]] -name = "google-cloud-core" -version = "2.4.1" -description = "Google Cloud API client core library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"}, - {file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"}, -] - -[package.dependencies] -google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.25.0,<3.0dev" - -[package.extras] -grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] - -[[package]] -name = "google-cloud-resource-manager" -version = "1.14.2" -description = "Google Cloud Resource Manager API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google_cloud_resource_manager-1.14.2-py3-none-any.whl", hash = "sha256:d0fa954dedd1d2b8e13feae9099c01b8aac515b648e612834f9942d2795a9900"}, - {file = "google_cloud_resource_manager-1.14.2.tar.gz", hash = "sha256:962e2d904c550d7bac48372607904ff7bb3277e3bb4a36d80cc9a37e28e6eb74"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" -grpc-google-iam-v1 = ">=0.14.0,<1.0.0" -proto-plus = [ - {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, - {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, -] -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[[package]] -name = "google-cloud-storage" -version = "2.18.1" -description = "Google Cloud Storage API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google_cloud_storage-2.18.1-py2.py3-none-any.whl", hash = "sha256:9d8db6bde3a979cca7150511cd0e4cb363e5f69d31259d890ba1124fa109418c"}, - {file = "google_cloud_storage-2.18.1.tar.gz", hash = "sha256:6707a6f30a05aee36faca81296419ca2907ac750af1c0457f278bc9a6fb219ad"}, -] - -[package.dependencies] -google-api-core = ">=2.15.0,<3.0.0dev" -google-auth = ">=2.26.1,<3.0dev" -google-cloud-core = ">=2.3.0,<3.0dev" -google-crc32c = ">=1.0,<2.0dev" -google-resumable-media = ">=2.6.0" -requests = ">=2.18.0,<3.0.0dev" - -[package.extras] -protobuf = ["protobuf (<6.0.0dev)"] -tracing = ["opentelemetry-api (>=1.1.0)"] - -[[package]] -name = "google-crc32c" -version = "1.5.0" -description = "A python wrapper of the C library 'Google CRC32C'" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-crc32c-1.5.0.tar.gz", hash = "sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7"}, - {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13"}, - {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c"}, - {file = "google_crc32c-1.5.0-cp310-cp310-win32.whl", hash = "sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee"}, - {file = "google_crc32c-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289"}, - {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273"}, - {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c"}, - {file = "google_crc32c-1.5.0-cp311-cp311-win32.whl", hash = "sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709"}, - {file = "google_crc32c-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740"}, - {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8"}, - {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-win32.whl", hash = "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4"}, - {file = "google_crc32c-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c"}, - {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7"}, - {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61"}, - {file = "google_crc32c-1.5.0-cp39-cp39-win32.whl", hash = "sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c"}, - {file = "google_crc32c-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93"}, -] - -[package.extras] -testing = ["pytest"] - -[[package]] -name = "google-resumable-media" -version = "2.7.0" -description = "Utilities for Google Media Downloads and Resumable Uploads" -optional = false -python-versions = ">= 3.7" -files = [ - {file = "google-resumable-media-2.7.0.tar.gz", hash = "sha256:5f18f5fa9836f4b083162064a1c2c98c17239bfda9ca50ad970ccf905f3e625b"}, - {file = "google_resumable_media-2.7.0-py2.py3-none-any.whl", hash = "sha256:79543cfe433b63fd81c0844b7803aba1bb8950b47bedf7d980c38fa123937e08"}, -] - -[package.dependencies] -google-crc32c = ">=1.0,<2.0dev" - -[package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] -requests = ["requests (>=2.18.0,<3.0.0dev)"] - -[[package]] -name = "google-search-results" -version = "2.4.2" -description = "Scrape and search localized results from Google, Bing, Baidu, Yahoo, Yandex, Ebay, Homedepot, youtube at scale using SerpApi.com" -optional = false -python-versions = ">=3.5" -files = [ - {file = "google_search_results-2.4.2.tar.gz", hash = "sha256:603a30ecae2af8e600b22635757a6df275dad4b934f975e67878ccd640b78245"}, -] - -[package.dependencies] -requests = "*" - -[[package]] -name = "googleapis-common-protos" -version = "1.70.0" -description = "Common protobufs used in Google APIs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, - {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, -] - -[package.dependencies] -grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0)"] - -[[package]] -name = "greenlet" -version = "3.0.3" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - -[[package]] -name = "griffe" -version = "1.7.3" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.9" -files = [ - {file = "griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75"}, - {file = "griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b"}, -] - -[package.dependencies] -colorama = ">=0.4" - -[[package]] -name = "groq" -version = "0.5.0" -description = "The official Python library for the groq API" -optional = false -python-versions = ">=3.7" -files = [ - {file = "groq-0.5.0-py3-none-any.whl", hash = "sha256:a7e6be1118bcdfea3ed071ec00f505a34d4e6ec28c435adb5a5afd33545683a1"}, - {file = "groq-0.5.0.tar.gz", hash = "sha256:d476cdc3383b45d2a4dc1876142a9542e663ea1029f9e07a05de24f895cae48c"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -typing-extensions = ">=4.7,<5" - -[[package]] -name = "grpc-google-iam-v1" -version = "0.14.2" -description = "IAM API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "grpc_google_iam_v1-0.14.2-py3-none-any.whl", hash = "sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351"}, - {file = "grpc_google_iam_v1-0.14.2.tar.gz", hash = "sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20"}, -] - -[package.dependencies] -googleapis-common-protos = {version = ">=1.56.0,<2.0.0", extras = ["grpc"]} -grpcio = ">=1.44.0,<2.0.0" -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[[package]] -name = "grpcio" -version = "1.71.0" -description = "HTTP/2-based RPC framework" -optional = false -python-versions = ">=3.9" -files = [ - {file = "grpcio-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd"}, - {file = "grpcio-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:0ab8b2864396663a5b0b0d6d79495657ae85fa37dcb6498a2669d067c65c11ea"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c30f393f9d5ff00a71bb56de4aa75b8fe91b161aeb61d39528db6b768d7eac69"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f250ff44843d9a0615e350c77f890082102a0318d66a99540f54769c8766ab73"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6d8de076528f7c43a2f576bc311799f89d795aa6c9b637377cc2b1616473804"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b91879d6da1605811ebc60d21ab6a7e4bae6c35f6b63a061d61eb818c8168f6"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f71574afdf944e6652203cd1badcda195b2a27d9c83e6d88dc1ce3cfb73b31a5"}, - {file = "grpcio-1.71.0-cp310-cp310-win32.whl", hash = "sha256:8997d6785e93308f277884ee6899ba63baafa0dfb4729748200fcc537858a509"}, - {file = "grpcio-1.71.0-cp310-cp310-win_amd64.whl", hash = "sha256:7d6ac9481d9d0d129224f6d5934d5832c4b1cddb96b59e7eba8416868909786a"}, - {file = "grpcio-1.71.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:d6aa986318c36508dc1d5001a3ff169a15b99b9f96ef5e98e13522c506b37eef"}, - {file = "grpcio-1.71.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:d2c170247315f2d7e5798a22358e982ad6eeb68fa20cf7a820bb74c11f0736e7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:e6f83a583ed0a5b08c5bc7a3fe860bb3c2eac1f03f1f63e0bc2091325605d2b7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be74ddeeb92cc87190e0e376dbc8fc7736dbb6d3d454f2fa1f5be1dee26b9d7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd0dfbe4d5eb1fcfec9490ca13f82b089a309dc3678e2edabc144051270a66e"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a2242d6950dc892afdf9e951ed7ff89473aaf744b7d5727ad56bdaace363722b"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0fa05ee31a20456b13ae49ad2e5d585265f71dd19fbd9ef983c28f926d45d0a7"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3d081e859fb1ebe176de33fc3adb26c7d46b8812f906042705346b314bde32c3"}, - {file = "grpcio-1.71.0-cp311-cp311-win32.whl", hash = "sha256:d6de81c9c00c8a23047136b11794b3584cdc1460ed7cbc10eada50614baa1444"}, - {file = "grpcio-1.71.0-cp311-cp311-win_amd64.whl", hash = "sha256:24e867651fc67717b6f896d5f0cac0ec863a8b5fb7d6441c2ab428f52c651c6b"}, - {file = "grpcio-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537"}, - {file = "grpcio-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79"}, - {file = "grpcio-1.71.0-cp312-cp312-win32.whl", hash = "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a"}, - {file = "grpcio-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8"}, - {file = "grpcio-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379"}, - {file = "grpcio-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637"}, - {file = "grpcio-1.71.0-cp313-cp313-win32.whl", hash = "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb"}, - {file = "grpcio-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366"}, - {file = "grpcio-1.71.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c6a0a28450c16809f94e0b5bfe52cabff63e7e4b97b44123ebf77f448534d07d"}, - {file = "grpcio-1.71.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:a371e6b6a5379d3692cc4ea1cb92754d2a47bdddeee755d3203d1f84ae08e03e"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:39983a9245d37394fd59de71e88c4b295eb510a3555e0a847d9965088cdbd033"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9182e0063112e55e74ee7584769ec5a0b4f18252c35787f48738627e23a62b97"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693bc706c031aeb848849b9d1c6b63ae6bcc64057984bb91a542332b75aa4c3d"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20e8f653abd5ec606be69540f57289274c9ca503ed38388481e98fa396ed0b41"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8700a2a57771cc43ea295296330daaddc0d93c088f0a35cc969292b6db959bf3"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d35a95f05a8a2cbe8e02be137740138b3b2ea5f80bd004444e4f9a1ffc511e32"}, - {file = "grpcio-1.71.0-cp39-cp39-win32.whl", hash = "sha256:f9c30c464cb2ddfbc2ddf9400287701270fdc0f14be5f08a1e3939f1e749b455"}, - {file = "grpcio-1.71.0-cp39-cp39-win_amd64.whl", hash = "sha256:63e41b91032f298b3e973b3fa4093cbbc620c875e2da7b93e249d4728b54559a"}, - {file = "grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c"}, -] - -[package.extras] -protobuf = ["grpcio-tools (>=1.71.0)"] - -[[package]] -name = "grpcio-status" -version = "1.62.2" -description = "Status proto mapping for gRPC" -optional = false -python-versions = ">=3.6" -files = [ - {file = "grpcio-status-1.62.2.tar.gz", hash = "sha256:62e1bfcb02025a1cd73732a2d33672d3e9d0df4d21c12c51e0bbcaf09bab742a"}, - {file = "grpcio_status-1.62.2-py3-none-any.whl", hash = "sha256:206ddf0eb36bc99b033f03b2c8e95d319f0044defae9b41ae21408e7e0cda48f"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.5.5" -grpcio = ">=1.62.2" -protobuf = ">=4.21.6" - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httptools" -version = "0.6.1" -description = "A collection of framework independent HTTP protocol utils." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, - {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, - {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, - {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, - {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, - {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, - {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, -] - -[package.extras] -test = ["Cython (>=0.29.24,<0.30.0)"] - -[[package]] -name = "httpx" -version = "0.27.0" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - -[[package]] -name = "httpx-sse" -version = "0.4.0" -description = "Consume Server-Sent Event (SSE) messages with HTTPX." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, -] - -[[package]] -name = "huggingface-hub" -version = "0.24.5" -description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "huggingface_hub-0.24.5-py3-none-any.whl", hash = "sha256:d93fb63b1f1a919a22ce91a14518974e81fc4610bf344dfe7572343ce8d3aced"}, - {file = "huggingface_hub-0.24.5.tar.gz", hash = "sha256:7b45d6744dd53ce9cbf9880957de00e9d10a9ae837f1c9b7255fc8fa4e8264f3"}, -] - -[package.dependencies] -filelock = "*" -fsspec = ">=2023.5.0" -packaging = ">=20.9" -pyyaml = ">=5.1" -requests = "*" -tqdm = ">=4.42.1" -typing-extensions = ">=3.7.4.3" - -[package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] -hf-transfer = ["hf-transfer (>=0.1.4)"] -inference = ["aiohttp", "minijinja (>=1.0)"] -quality = ["mypy (==1.5.1)", "ruff (>=0.5.0)"] -tensorflow = ["graphviz", "pydot", "tensorflow"] -tensorflow-testing = ["keras (<3.0)", "tensorflow"] -testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] -torch = ["safetensors[torch]", "torch"] -typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] - -[[package]] -name = "humanfriendly" -version = "10.0" -description = "Human friendly output for text interfaces using Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, - {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, -] - -[package.dependencies] -pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} - -[[package]] -name = "identify" -version = "2.5.36" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, - {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.7" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] - -[[package]] -name = "importlib-metadata" -version = "7.0.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"}, - {file = "importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "importlib-resources" -version = "6.4.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, - {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jiter" -version = "0.4.2" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jiter-0.4.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c2b003ff58d14f5e182b875acd5177b2367245c19a03be9a2230535d296f7550"}, - {file = "jiter-0.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b48c77c25f094707731cd5bad6b776046846b60a27ee20efc8fadfb10a89415f"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f50ad6b172bde4d45f4d4ea10c49282a337b8bb735afc99763dfa55ea84a743"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f6001e86f525fbbc9706db2078dc22be078b0950de55b92d37041930f5f940"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16646ef23b62b007de80460d303ebb2d81e355dac9389c787cec87cdd7ffef2f"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b4e847c13b0bf1255c711a92330e7a8cb8b5cdd1e37d7db309627bcdd3367ff"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c536589be60e4c5f2b20fadc4db7e9f55d4c9df3551f29ddf1c4a18dcc9dd54"}, - {file = "jiter-0.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3b2763996167830889a854b4ded30bb90897f9b76be78069c50c3ec4540950e"}, - {file = "jiter-0.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:675e8ab98c99495091af6b6e9bf2b6353bcf81f25ab6ce27d36127e315b4505d"}, - {file = "jiter-0.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e48e43d9d999aaf55f53406b8846ff8cbe3e47ee4b9dc37e5a10a65ce760809f"}, - {file = "jiter-0.4.2-cp310-none-win32.whl", hash = "sha256:881b6e67c50bc36acb3570eda693763c8cd77d590940e06fa6d325d0da52ec1b"}, - {file = "jiter-0.4.2-cp310-none-win_amd64.whl", hash = "sha256:bb8f7b43259efc6add0d721ade2953e064b24e2026d26d979bc09ec080844cef"}, - {file = "jiter-0.4.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:24ad336ac47f274fa83f6fbedcabff9d3387c80f67c66b992688e6a8ba2c47e9"}, - {file = "jiter-0.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc392a220095730afe365ce1516f2f88bb085a2fd29ea191be9c6e3c71713d9a"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1fdc408de36c81460896de0176f2f7b9f3574dcd35693a0b2c00f4ca34c98e4"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10ad76722ee6a8c820b0db06a793c08b7d679e5201b9563015bd1e06c959a09"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbb46d1e9c82bba87f0cbda38413e49448a7df35b1e55917124bff9f38974a23"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:194e28ef4b5f3b61408cb2ee6b6dcbcdb0c9063d01b92b01345b7605692849f5"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0a447533eccd62748a727e058efa10a8d7cf1de8ffe1a4d705ecb41dad9090"}, - {file = "jiter-0.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5f7704d7260bbb88cca3453951af739589132b26e896a3144fa2dae2263716d7"}, - {file = "jiter-0.4.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:01427458bc9550f2eda09d425755330e7d0eb09adce099577433bebf05d28d59"}, - {file = "jiter-0.4.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:159b8416879c0053b17c352f70b67b749ef5b2924c6154318ecf71918aab0905"}, - {file = "jiter-0.4.2-cp311-none-win32.whl", hash = "sha256:f2445234acfb79048ce1a0d5d0e181abb9afd9e4a29d8d9988fe26cc5773a81a"}, - {file = "jiter-0.4.2-cp311-none-win_amd64.whl", hash = "sha256:e15a65f233b6b0e5ac10ddf3b97ceb18aa9ffba096259961641d78b4ee321bd5"}, - {file = "jiter-0.4.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d61d59521aea9745447ce50f74d39a16ef74ec9d6477d9350d77e75a3d774ad2"}, - {file = "jiter-0.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eef607dc0acc251923427808dbd017f1998ae3c1a0430a261527aa5cbb3a942"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af6bf39954646e374fc47429c656372ac731a6a26b644158a5a84bcdbed33a47"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f509d23606e476852ee46a2b65b5c4ad3905f17424d9cc19c1dffa1c94ba3c6"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59672774daa44ee140aada0c781c82bee4d9ac5e522966186cfb6b3c217d8a51"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a0458efac5afeca254cf557b8a654e17013075a69905c78f88d557f129d871"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8860766d1c293e75c1bb4e25b74fa987e3adf199cac3f5f9e6e49c2bebf092f"}, - {file = "jiter-0.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a109f3281b72bbf4921fe43db1005c004a38559ca0b6c4985add81777dfe0a44"}, - {file = "jiter-0.4.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:faa7e667454b77ad2f0ef87db39f4944de759617aadf210ea2b73f26bb24755f"}, - {file = "jiter-0.4.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3512f8b00cafb6780b427cb6282800d2bf8277161d9c917830661bd4ed1d3528"}, - {file = "jiter-0.4.2-cp312-none-win32.whl", hash = "sha256:853b35d508ee5b66d06630473c1c0b7bb5e29bf4785c9d2202437116c94f7e21"}, - {file = "jiter-0.4.2-cp312-none-win_amd64.whl", hash = "sha256:4a3a8197784278eb8b24cb02c45e1cad67c2ce5b5b758adfb19b87f74bbdff9c"}, - {file = "jiter-0.4.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ca2a4d750aed3154b89f2efb148609fc985fad8db739460797aaf9b478acedda"}, - {file = "jiter-0.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e6c304b3cc6896256727e1fb8991c7179a345eca8224e201795e9cacf4683b0"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cc34ac708ae1750d077e490321761ec4b9a055b994cbdd1d6fbd37099e4aa7b"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c93383875ab8d2e4f760aaff335b4a12ff32d4f9cf49c4498d657734f611466"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce197ee044add576afca0955b42142dd0312639adb6ebadbdbe4277f2855614f"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a427716813ff65480ca5b5117cfa099f49b49cd38051f8609bd0d5493013ca0"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479990218353356234669e70fac53e5eb6f739a10db25316171aede2c97d9364"}, - {file = "jiter-0.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d35a91ec5ac74cf33234c431505299fa91c0a197c2dbafd47400aca7c69489d4"}, - {file = "jiter-0.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b27189847193708c94ad10ca0d891309342ae882725d2187cf5d2db02bde8d1b"}, - {file = "jiter-0.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76c255308cd1093fb411a03756b7bb220e48d4a98c30cbc79ed448bf3978e27d"}, - {file = "jiter-0.4.2-cp38-none-win32.whl", hash = "sha256:bb77438060bad49cc251941e6701b31138365c8a0ddaf10cdded2fcc6dd30701"}, - {file = "jiter-0.4.2-cp38-none-win_amd64.whl", hash = "sha256:ce858af19f7ce0d4b51c9f6c0c9d08f1e9dcef1986c5875efd0674a7054292ca"}, - {file = "jiter-0.4.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:6128838a2f357b3921b2a3242d5dc002ae4255ecc8f9f05c20d56d7d2d79c5ad"}, - {file = "jiter-0.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2420cebb9ba856cb57dcab1d2d8def949b464b0db09c22a4e4dbd52fff7b200"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d13d8128e853b320e00bb18bd4bb8b136cc0936091dc87633648fc688eb705"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eba5d6e54f149c508ba88677f97d3dc7dd75e9980d234bbac8027ac6db0763a3"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fad5d64af0bc0545237419bf4150d8de56f0bd217434bdd1a59730327252bef"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d179e7bca89cf5719bd761dd37a341ff0f98199ecaa9c14af09792e47e977cc"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36353caee9f103d8ee7bda077f6400505b0f370e27eabcab33a33d21de12a2a6"}, - {file = "jiter-0.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dd146c25bce576ca5db64fc7eccb8862af00f1f0e30108796953f12a53660e4c"}, - {file = "jiter-0.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:14b7c08cadbcd703041c66dc30e24e17de2f340281cac0e69374223ecf153aa4"}, - {file = "jiter-0.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a90f1a8b3d29aea198f8ea2b01148276ced8056e5103f32525266b3d880e65c9"}, - {file = "jiter-0.4.2-cp39-none-win32.whl", hash = "sha256:25b174997c780337b61ae57b1723455eecae9a17a9659044fd3c3b369190063f"}, - {file = "jiter-0.4.2-cp39-none-win_amd64.whl", hash = "sha256:bef62cea18521c5b99368147040c7e560c55098a35c93456f110678a2d34189a"}, - {file = "jiter-0.4.2.tar.gz", hash = "sha256:29b9d44f23f0c05f46d482f4ebf03213ee290d77999525d0975a17f875bf1eea"}, -] - -[[package]] -name = "jmespath" -version = "1.0.1" -description = "JSON Matching Expressions" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] - -[[package]] -name = "joblib" -version = "1.4.2" -description = "Lightweight pipelining with Python functions" -optional = false -python-versions = ">=3.8" -files = [ - {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, - {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, -] - -[[package]] -name = "jsonpatch" -version = "1.33" -description = "Apply JSON-Patches (RFC 6902)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -files = [ - {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, - {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, -] - -[package.dependencies] -jsonpointer = ">=1.9" - -[[package]] -name = "jsonpointer" -version = "2.4" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -files = [ - {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, - {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, -] - -[[package]] -name = "kubernetes" -version = "29.0.0" -description = "Kubernetes python client" -optional = false -python-versions = ">=3.6" -files = [ - {file = "kubernetes-29.0.0-py2.py3-none-any.whl", hash = "sha256:ab8cb0e0576ccdfb71886366efb102c6a20f268d817be065ce7f9909c631e43e"}, - {file = "kubernetes-29.0.0.tar.gz", hash = "sha256:c4812e227ae74d07d53c88293e564e54b850452715a59a927e7e1bc6b9a60459"}, -] - -[package.dependencies] -certifi = ">=14.05.14" -google-auth = ">=1.0.1" -oauthlib = ">=3.2.2" -python-dateutil = ">=2.5.3" -pyyaml = ">=5.4.1" -requests = "*" -requests-oauthlib = "*" -six = ">=1.9.0" -urllib3 = ">=1.24.2" -websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" - -[package.extras] -adal = ["adal (>=1.0.2)"] - -[[package]] -name = "langchain" -version = "0.3.12" -description = "Building applications with LLMs through composability" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain-0.3.12-py3-none-any.whl", hash = "sha256:581ad93a9de12e4b957bc2af9ba8482eb86e3930e84c4ee20ed677da5e2311cd"}, - {file = "langchain-0.3.12.tar.gz", hash = "sha256:0d8247afbf37beb263b4adc29f7aa8a5ae83c43a6941894e2f9ba39d5c869e3b"}, -] - -[package.dependencies] -aiohttp = ">=3.8.3,<4.0.0" -async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""} -langchain-core = ">=0.3.25,<0.4.0" -langchain-text-splitters = ">=0.3.3,<0.4.0" -langsmith = ">=0.1.17,<0.3" -numpy = [ - {version = ">=1.22.4,<2", markers = "python_version < \"3.12\""}, - {version = ">=1.26.2,<3", markers = "python_version >= \"3.12\""}, -] -pydantic = ">=2.7.4,<3.0.0" -PyYAML = ">=5.3" -requests = ">=2,<3" -SQLAlchemy = ">=1.4,<3" -tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10" - -[[package]] -name = "langchain-anthropic" -version = "0.3.0" -description = "An integration package connecting AnthropicMessages and LangChain" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_anthropic-0.3.0-py3-none-any.whl", hash = "sha256:96b74a9adfcc092cc2ae137d4189ca50e8f5ad9635618024f7c98d8f9fc1076a"}, - {file = "langchain_anthropic-0.3.0.tar.gz", hash = "sha256:f9b5cbdbf2d5b3432f78f056e474efb10a2c1e37f9a471d3aceb50a0d9f945df"}, -] - -[package.dependencies] -anthropic = ">=0.39.0,<1" -defusedxml = ">=0.7.1,<0.8.0" -langchain-core = ">=0.3.17,<0.4.0" -pydantic = ">=2.7.4,<3.0.0" - -[[package]] -name = "langchain-aws" -version = "0.2.9" -description = "An integration package connecting AWS and LangChain" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_aws-0.2.9-py3-none-any.whl", hash = "sha256:e6b70017e731d44c3c24c90025b7b199e524c042ae8f8be7094d8bd34ae3adb3"}, - {file = "langchain_aws-0.2.9.tar.gz", hash = "sha256:bcc1d4c62addf4ee6cca4e441c63269003e1485069193991e27cae5579bc0353"}, -] - -[package.dependencies] -boto3 = ">=1.35.74" -langchain-core = ">=0.3.15,<0.4" -numpy = [ - {version = ">=1,<2", markers = "python_version < \"3.12\""}, - {version = ">=1.26.0,<3", markers = "python_version >= \"3.12\""}, -] -pydantic = ">=2,<3" - -[[package]] -name = "langchain-cohere" -version = "0.3.3" -description = "An integration package connecting Cohere and LangChain" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_cohere-0.3.3-py3-none-any.whl", hash = "sha256:c8dee47a31cedb227ccf3ba93dad5f09ebadf9043e0ce941ae0bffdc3a226b37"}, - {file = "langchain_cohere-0.3.3.tar.gz", hash = "sha256:502f35eb5f983656b26114c7411628241fd06f14e24c85721ea57c9ee1c7c890"}, -] - -[package.dependencies] -cohere = ">=5.5.6,<6.0" -langchain-core = ">=0.3.0,<0.4" -langchain-experimental = ">=0.3.0,<0.4.0" -pandas = ">=1.4.3" -pydantic = ">=2,<3" -tabulate = ">=0.9.0,<0.10.0" - -[package.extras] -langchain-community = ["langchain-community (>=0.3.0,<0.4.0)"] - -[[package]] -name = "langchain-community" -version = "0.3.12" -description = "Community contributed LangChain integrations." -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_community-0.3.12-py3-none-any.whl", hash = "sha256:5a993c931d46dc07fcdfcdfa4d87095c5a15d37ff32b0c16e9ecf6f5caa58c9c"}, - {file = "langchain_community-0.3.12.tar.gz", hash = "sha256:b4694f34c7214dede03fe5a75e9f335e16bd788dfa6ca279302ad357bf0d0fc4"}, -] - -[package.dependencies] -aiohttp = ">=3.8.3,<4.0.0" -dataclasses-json = ">=0.5.7,<0.7" -httpx-sse = ">=0.4.0,<0.5.0" -langchain = ">=0.3.12,<0.4.0" -langchain-core = ">=0.3.25,<0.4.0" -langsmith = ">=0.1.125,<0.3" -numpy = [ - {version = ">=1.22.4,<2", markers = "python_version < \"3.12\""}, - {version = ">=1.26.2,<3", markers = "python_version >= \"3.12\""}, -] -pydantic-settings = ">=2.4.0,<3.0.0" -PyYAML = ">=5.3" -requests = ">=2,<3" -SQLAlchemy = ">=1.4,<3" -tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10" - -[[package]] -name = "langchain-core" -version = "0.3.25" -description = "Building applications with LLMs through composability" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_core-0.3.25-py3-none-any.whl", hash = "sha256:e10581c6c74ba16bdc6fdf16b00cced2aa447cc4024ed19746a1232918edde38"}, - {file = "langchain_core-0.3.25.tar.gz", hash = "sha256:fdb8df41e5cdd928c0c2551ebbde1cea770ee3c64598395367ad77ddf9acbae7"}, -] - -[package.dependencies] -jsonpatch = ">=1.33,<2.0" -langsmith = ">=0.1.125,<0.3" -packaging = ">=23.2,<25" -pydantic = [ - {version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""}, - {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, -] -PyYAML = ">=5.3" -tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0" -typing-extensions = ">=4.7" - -[[package]] -name = "langchain-experimental" -version = "0.3.3" -description = "Building applications with LLMs through composability" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_experimental-0.3.3-py3-none-any.whl", hash = "sha256:da01aafc162631475f306ca368ecae74d5becd93b8039bddb6315e755e274580"}, - {file = "langchain_experimental-0.3.3.tar.gz", hash = "sha256:6bbcdcd084581432ef4b5d732294a59d75a858ede1714b50a5b79bcfe31fa306"}, -] - -[package.dependencies] -langchain-community = ">=0.3.0,<0.4.0" -langchain-core = ">=0.3.15,<0.4.0" - -[[package]] -name = "langchain-google-vertexai" -version = "2.0.9" -description = "An integration package connecting Google VertexAI and LangChain" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_google_vertexai-2.0.9-py3-none-any.whl", hash = "sha256:cb1085cf75793bd03fc825363f2cad9706eb1c30fd263da1998098e4c01e9c8f"}, - {file = "langchain_google_vertexai-2.0.9.tar.gz", hash = "sha256:da4a2c08d84a392fa6ab09d8f3f45eb5e86fd5e063ed7b4085c9f62fb04dc1ca"}, -] - -[package.dependencies] -google-cloud-aiplatform = ">=1.70.0,<2.0.0" -google-cloud-storage = ">=2.18.0,<3.0.0" -httpx = ">=0.27.0,<0.28.0" -httpx-sse = ">=0.4.0,<0.5.0" -langchain-core = ">=0.3.15,<0.4" -pydantic = ">=2.9,<2.10" - -[package.extras] -anthropic = ["anthropic[vertexai] (>=0.35.0,<1)"] -mistral = ["langchain-mistralai (>=0.2.0,<1)"] - -[[package]] -name = "langchain-groq" -version = "0.2.1" -description = "An integration package connecting Groq and LangChain" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_groq-0.2.1-py3-none-any.whl", hash = "sha256:98d282fd9d7d99b0f55de0a1daea2d5d350ef697e3cb5e97de06aeba4eca8679"}, - {file = "langchain_groq-0.2.1.tar.gz", hash = "sha256:a59c81d1a15dc97abf4fdb4c2589f98109313eda147e6b378829222d4d929792"}, -] - -[package.dependencies] -groq = ">=0.4.1,<1" -langchain-core = ">=0.3.15,<0.4.0" - -[[package]] -name = "langchain-mistralai" -version = "0.2.3" -description = "An integration package connecting Mistral and LangChain" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_mistralai-0.2.3-py3-none-any.whl", hash = "sha256:960abdd540af527da7e4d68b9316355ece6a740a60d145f32e4e19c1a9fe4d3a"}, - {file = "langchain_mistralai-0.2.3.tar.gz", hash = "sha256:2033af92fd82ac7915664885a94579edec19feb3122525fff8dd2787e174c087"}, -] - -[package.dependencies] -httpx = ">=0.25.2,<1" -httpx-sse = ">=0.3.1,<1" -langchain-core = ">=0.3.21,<0.4.0" -pydantic = ">=2,<3" -tokenizers = ">=0.15.1,<1" - -[[package]] -name = "langchain-ollama" -version = "0.2.1" -description = "An integration package connecting Ollama and LangChain" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_ollama-0.2.1-py3-none-any.whl", hash = "sha256:033916150cc9c341d72274512b9987a0ebf014cf808237687012fc7af4a81ee3"}, - {file = "langchain_ollama-0.2.1.tar.gz", hash = "sha256:752b112d233a6e079259cb10138a5af836f42d26781cac6d7eb1b1e0d2ae9a0d"}, -] - -[package.dependencies] -langchain-core = ">=0.3.20,<0.4.0" -ollama = ">=0.3.0,<1" - -[[package]] -name = "langchain-openai" -version = "0.2.11" -description = "An integration package connecting OpenAI and LangChain" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_openai-0.2.11-py3-none-any.whl", hash = "sha256:c019ae915a5782943bee9503388e65c8622d400e0451ef885f3e4989cf35727f"}, - {file = "langchain_openai-0.2.11.tar.gz", hash = "sha256:563bd843092d260c7ffd88b8e0e6b830f36347e058e62a6d5e9cc4c461a8da98"}, -] - -[package.dependencies] -langchain-core = ">=0.3.21,<0.4.0" -openai = ">=1.54.0,<2.0.0" -tiktoken = ">=0.7,<1" - -[[package]] -name = "langchain-text-splitters" -version = "0.3.3" -description = "LangChain text splitting utilities" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "langchain_text_splitters-0.3.3-py3-none-any.whl", hash = "sha256:c2f8650457685072971edc8c52c9f8826496b3307f28004a7fd09eb32d4d819f"}, - {file = "langchain_text_splitters-0.3.3.tar.gz", hash = "sha256:c596958dcab15fdfe0627fd36ce9d588d0a7e35593af70cd10d0a4a06d69b3ee"}, -] - -[package.dependencies] -langchain-core = ">=0.3.25,<0.4.0" - -[[package]] -name = "langgraph" -version = "0.2.62" -description = "Building stateful, multi-actor applications with LLMs" -optional = false -python-versions = "<4.0,>=3.9.0" -files = [ - {file = "langgraph-0.2.62-py3-none-any.whl", hash = "sha256:51ae9e02a52485a837642eebe7ae43269af7d7305d62f8f69ac11589b2fbba26"}, - {file = "langgraph-0.2.62.tar.gz", hash = "sha256:0aac9fd55ffe669bc1312203e0f9ea2733c65cc276f196e7ff0d443cf4efbb89"}, -] - -[package.dependencies] -langchain-core = ">=0.2.43,<0.3.0 || >0.3.0,<0.3.1 || >0.3.1,<0.3.2 || >0.3.2,<0.3.3 || >0.3.3,<0.3.4 || >0.3.4,<0.3.5 || >0.3.5,<0.3.6 || >0.3.6,<0.3.7 || >0.3.7,<0.3.8 || >0.3.8,<0.3.9 || >0.3.9,<0.3.10 || >0.3.10,<0.3.11 || >0.3.11,<0.3.12 || >0.3.12,<0.3.13 || >0.3.13,<0.3.14 || >0.3.14,<0.3.15 || >0.3.15,<0.3.16 || >0.3.16,<0.3.17 || >0.3.17,<0.3.18 || >0.3.18,<0.3.19 || >0.3.19,<0.3.20 || >0.3.20,<0.3.21 || >0.3.21,<0.3.22 || >0.3.22,<0.4.0" -langgraph-checkpoint = ">=2.0.4,<3.0.0" -langgraph-sdk = ">=0.1.42,<0.2.0" - -[[package]] -name = "langgraph-checkpoint" -version = "2.0.9" -description = "Library with base interfaces for LangGraph checkpoint savers." -optional = false -python-versions = "<4.0.0,>=3.9.0" -files = [ - {file = "langgraph_checkpoint-2.0.9-py3-none-any.whl", hash = "sha256:b546ed6129929b8941ac08af6ce5cd26c8ebe1d25883d3c48638d34ade91ce42"}, - {file = "langgraph_checkpoint-2.0.9.tar.gz", hash = "sha256:43847d7e385a2d9d2b684155920998e44ed42d2d1780719e4f6111fe3d6db84c"}, -] - -[package.dependencies] -langchain-core = ">=0.2.38,<0.4" -msgpack = ">=1.1.0,<2.0.0" - -[[package]] -name = "langgraph-sdk" -version = "0.1.51" -description = "SDK for interacting with LangGraph API" -optional = false -python-versions = "<4.0.0,>=3.9.0" -files = [ - {file = "langgraph_sdk-0.1.51-py3-none-any.whl", hash = "sha256:ce2b58466d1700d06149782ed113157a8694a6d7932c801f316cd13fab315fe4"}, - {file = "langgraph_sdk-0.1.51.tar.gz", hash = "sha256:dea1363e72562cb1e82a2d156be8d5b1a69ff3fe8815eee0e1e7a2f423242ec1"}, -] - -[package.dependencies] -httpx = ">=0.25.2" -orjson = ">=3.10.1" - -[[package]] -name = "langsmith" -version = "0.1.146" -description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." -optional = false -python-versions = "<4.0,>=3.8.1" -files = [ - {file = "langsmith-0.1.146-py3-none-any.whl", hash = "sha256:9d062222f1a32c9b047dab0149b24958f988989cd8d4a5f9139ff959a51e59d8"}, - {file = "langsmith-0.1.146.tar.gz", hash = "sha256:ead8b0b9d5b6cd3ac42937ec48bdf09d4afe7ca1bba22dc05eb65591a18106f8"}, -] - -[package.dependencies] -httpx = ">=0.23.0,<1" -orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""} -pydantic = [ - {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, - {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, -] -requests = ">=2,<3" -requests-toolbelt = ">=1.0.0,<2.0.0" - -[[package]] -name = "lark" -version = "1.1.9" -description = "a modern parsing library" -optional = false -python-versions = ">=3.6" -files = [ - {file = "lark-1.1.9-py3-none-any.whl", hash = "sha256:a0dd3a87289f8ccbb325901e4222e723e7d745dbfc1803eaf5f3d2ace19cf2db"}, - {file = "lark-1.1.9.tar.gz", hash = "sha256:15fa5236490824c2c4aba0e22d2d6d823575dcaf4cdd1848e34b6ad836240fba"}, -] - -[package.extras] -atomic-cache = ["atomicwrites"] -interegular = ["interegular (>=0.3.1,<0.4.0)"] -nearley = ["js2py"] -regex = ["regex"] - -[[package]] -name = "llama-index-core" -version = "0.12.41" -description = "Interface between LLMs and your data" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "llama_index_core-0.12.41-py3-none-any.whl", hash = "sha256:61fc1e912303250ee32778d5db566e242f7bd9ee043940860a436e3c93f17ef5"}, - {file = "llama_index_core-0.12.41.tar.gz", hash = "sha256:34f77c51b12b11ee1d743ff183c1b61afe2975f9970357723a744d4e080f5b3d"}, -] - -[package.dependencies] -aiohttp = ">=3.8.6,<4" -aiosqlite = "*" -banks = ">=2.0.0,<3" -dataclasses-json = "*" -deprecated = ">=1.2.9.3" -dirtyjson = ">=1.0.8,<2" -eval-type-backport = {version = ">=0.2.0,<0.3", markers = "python_version < \"3.10\""} -filetype = ">=1.2.0,<2" -fsspec = ">=2023.5.0" -httpx = "*" -nest-asyncio = ">=1.5.8,<2" -networkx = ">=3.0" -nltk = ">3.8.1" -numpy = "*" -pillow = ">=9.0.0" -pydantic = ">=2.8.0" -pyyaml = ">=6.0.1" -requests = ">=2.31.0" -sqlalchemy = {version = ">=1.4.49", extras = ["asyncio"]} -tenacity = ">=8.2.0,<8.4.0 || >8.4.0,<10.0.0" -tiktoken = ">=0.7.0" -tqdm = ">=4.66.1,<5" -typing-extensions = ">=4.5.0" -typing-inspect = ">=0.8.0" -wrapt = "*" - -[[package]] -name = "llama-index-llms-anthropic" -version = "0.5.0" -description = "llama-index llms anthropic integration" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "llama_index_llms_anthropic-0.5.0-py3-none-any.whl", hash = "sha256:2b9367db45deabcbda4db1b1216c95e2663e1e6f129570fc2d275207dd3901cf"}, - {file = "llama_index_llms_anthropic-0.5.0.tar.gz", hash = "sha256:14e400ccc2deb8e9024ef8cdc24550b67f4240f3563b4564d4870be85de2d9d8"}, -] - -[package.dependencies] -anthropic = {version = ">=0.39.0", extras = ["bedrock", "vertex"]} -llama-index-core = ">=0.12.0,<0.13.0" - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "marshmallow" -version = "3.21.2" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -optional = false -python-versions = ">=3.8" -files = [ - {file = "marshmallow-3.21.2-py3-none-any.whl", hash = "sha256:70b54a6282f4704d12c0a41599682c5c5450e843b9ec406308653b47c59648a1"}, - {file = "marshmallow-3.21.2.tar.gz", hash = "sha256:82408deadd8b33d56338d2182d455db632c6313aa2af61916672146bb32edc56"}, -] - -[package.dependencies] -packaging = ">=17.0" - -[package.extras] -dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] -docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] -tests = ["pytest", "pytz", "simplejson"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mmh3" -version = "4.1.0" -description = "Python extension for MurmurHash (MurmurHash3), a set of fast and robust hash functions." -optional = false -python-versions = "*" -files = [ - {file = "mmh3-4.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be5ac76a8b0cd8095784e51e4c1c9c318c19edcd1709a06eb14979c8d850c31a"}, - {file = "mmh3-4.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98a49121afdfab67cd80e912b36404139d7deceb6773a83620137aaa0da5714c"}, - {file = "mmh3-4.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5259ac0535874366e7d1a5423ef746e0d36a9e3c14509ce6511614bdc5a7ef5b"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5950827ca0453a2be357696da509ab39646044e3fa15cad364eb65d78797437"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dd0f652ae99585b9dd26de458e5f08571522f0402155809fd1dc8852a613a39"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99d25548070942fab1e4a6f04d1626d67e66d0b81ed6571ecfca511f3edf07e6"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53db8d9bad3cb66c8f35cbc894f336273f63489ce4ac416634932e3cbe79eb5b"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75da0f615eb55295a437264cc0b736753f830b09d102aa4c2a7d719bc445ec05"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b926b07fd678ea84b3a2afc1fa22ce50aeb627839c44382f3d0291e945621e1a"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c5b053334f9b0af8559d6da9dc72cef0a65b325ebb3e630c680012323c950bb6"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bf33dc43cd6de2cb86e0aa73a1cc6530f557854bbbe5d59f41ef6de2e353d7b"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fa7eacd2b830727ba3dd65a365bed8a5c992ecd0c8348cf39a05cc77d22f4970"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:42dfd6742b9e3eec599f85270617debfa0bbb913c545bb980c8a4fa7b2d047da"}, - {file = "mmh3-4.1.0-cp310-cp310-win32.whl", hash = "sha256:2974ad343f0d39dcc88e93ee6afa96cedc35a9883bc067febd7ff736e207fa47"}, - {file = "mmh3-4.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:74699a8984ded645c1a24d6078351a056f5a5f1fe5838870412a68ac5e28d865"}, - {file = "mmh3-4.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f0dc874cedc23d46fc488a987faa6ad08ffa79e44fb08e3cd4d4cf2877c00a00"}, - {file = "mmh3-4.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3280a463855b0eae64b681cd5b9ddd9464b73f81151e87bb7c91a811d25619e6"}, - {file = "mmh3-4.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:97ac57c6c3301769e757d444fa7c973ceb002cb66534b39cbab5e38de61cd896"}, - {file = "mmh3-4.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b6502cdb4dbd880244818ab363c8770a48cdccecf6d729ade0241b736b5ec0"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ba2da04671a9621580ddabf72f06f0e72c1c9c3b7b608849b58b11080d8f14"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a5fef4c4ecc782e6e43fbeab09cff1bac82c998a1773d3a5ee6a3605cde343e"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5135358a7e00991f73b88cdc8eda5203bf9de22120d10a834c5761dbeb07dd13"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cff9ae76a54f7c6fe0167c9c4028c12c1f6de52d68a31d11b6790bb2ae685560"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f02576a4d106d7830ca90278868bf0983554dd69183b7bbe09f2fcd51cf54f"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:073d57425a23721730d3ff5485e2da489dd3c90b04e86243dd7211f889898106"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:71e32ddec7f573a1a0feb8d2cf2af474c50ec21e7a8263026e8d3b4b629805db"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7cbb20b29d57e76a58b40fd8b13a9130db495a12d678d651b459bf61c0714cea"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:a42ad267e131d7847076bb7e31050f6c4378cd38e8f1bf7a0edd32f30224d5c9"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4a013979fc9390abadc445ea2527426a0e7a4495c19b74589204f9b71bcaafeb"}, - {file = "mmh3-4.1.0-cp311-cp311-win32.whl", hash = "sha256:1d3b1cdad7c71b7b88966301789a478af142bddcb3a2bee563f7a7d40519a00f"}, - {file = "mmh3-4.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0dc6dc32eb03727467da8e17deffe004fbb65e8b5ee2b502d36250d7a3f4e2ec"}, - {file = "mmh3-4.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9ae3a5c1b32dda121c7dc26f9597ef7b01b4c56a98319a7fe86c35b8bc459ae6"}, - {file = "mmh3-4.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0033d60c7939168ef65ddc396611077a7268bde024f2c23bdc283a19123f9e9c"}, - {file = "mmh3-4.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d6af3e2287644b2b08b5924ed3a88c97b87b44ad08e79ca9f93d3470a54a41c5"}, - {file = "mmh3-4.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d82eb4defa245e02bb0b0dc4f1e7ee284f8d212633389c91f7fba99ba993f0a2"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba245e94b8d54765e14c2d7b6214e832557e7856d5183bc522e17884cab2f45d"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb04e2feeabaad6231e89cd43b3d01a4403579aa792c9ab6fdeef45cc58d4ec0"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e3b1a27def545ce11e36158ba5d5390cdbc300cfe456a942cc89d649cf7e3b2"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce0ab79ff736d7044e5e9b3bfe73958a55f79a4ae672e6213e92492ad5e734d5"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b02268be6e0a8eeb8a924d7db85f28e47344f35c438c1e149878bb1c47b1cd3"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:deb887f5fcdaf57cf646b1e062d56b06ef2f23421c80885fce18b37143cba828"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99dd564e9e2b512eb117bd0cbf0f79a50c45d961c2a02402787d581cec5448d5"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:08373082dfaa38fe97aa78753d1efd21a1969e51079056ff552e687764eafdfe"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:54b9c6a2ea571b714e4fe28d3e4e2db37abfd03c787a58074ea21ee9a8fd1740"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a7b1edf24c69e3513f879722b97ca85e52f9032f24a52284746877f6a7304086"}, - {file = "mmh3-4.1.0-cp312-cp312-win32.whl", hash = "sha256:411da64b951f635e1e2284b71d81a5a83580cea24994b328f8910d40bed67276"}, - {file = "mmh3-4.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bebc3ecb6ba18292e3d40c8712482b4477abd6981c2ebf0e60869bd90f8ac3a9"}, - {file = "mmh3-4.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:168473dd608ade6a8d2ba069600b35199a9af837d96177d3088ca91f2b3798e3"}, - {file = "mmh3-4.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:372f4b7e1dcde175507640679a2a8790185bb71f3640fc28a4690f73da986a3b"}, - {file = "mmh3-4.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:438584b97f6fe13e944faf590c90fc127682b57ae969f73334040d9fa1c7ffa5"}, - {file = "mmh3-4.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6e27931b232fc676675fac8641c6ec6b596daa64d82170e8597f5a5b8bdcd3b6"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:571a92bad859d7b0330e47cfd1850b76c39b615a8d8e7aa5853c1f971fd0c4b1"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a69d6afe3190fa08f9e3a58e5145549f71f1f3fff27bd0800313426929c7068"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afb127be0be946b7630220908dbea0cee0d9d3c583fa9114a07156f98566dc28"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:940d86522f36348ef1a494cbf7248ab3f4a1638b84b59e6c9e90408bd11ad729"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3dcccc4935686619a8e3d1f7b6e97e3bd89a4a796247930ee97d35ea1a39341"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01bb9b90d61854dfc2407c5e5192bfb47222d74f29d140cb2dd2a69f2353f7cc"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bcb1b8b951a2c0b0fb8a5426c62a22557e2ffc52539e0a7cc46eb667b5d606a9"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6477a05d5e5ab3168e82e8b106e316210ac954134f46ec529356607900aea82a"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:da5892287e5bea6977364b15712a2573c16d134bc5fdcdd4cf460006cf849278"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:99180d7fd2327a6fffbaff270f760576839dc6ee66d045fa3a450f3490fda7f5"}, - {file = "mmh3-4.1.0-cp38-cp38-win32.whl", hash = "sha256:9b0d4f3949913a9f9a8fb1bb4cc6ecd52879730aab5ff8c5a3d8f5b593594b73"}, - {file = "mmh3-4.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:598c352da1d945108aee0c3c3cfdd0e9b3edef74108f53b49d481d3990402169"}, - {file = "mmh3-4.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:475d6d1445dd080f18f0f766277e1237fa2914e5fe3307a3b2a3044f30892103"}, - {file = "mmh3-4.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ca07c41e6a2880991431ac717c2a049056fff497651a76e26fc22224e8b5732"}, - {file = "mmh3-4.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ebe052fef4bbe30c0548d12ee46d09f1b69035ca5208a7075e55adfe091be44"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaefd42e85afb70f2b855a011f7b4d8a3c7e19c3f2681fa13118e4d8627378c5"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0ae43caae5a47afe1b63a1ae3f0986dde54b5fb2d6c29786adbfb8edc9edfb"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6218666f74c8c013c221e7f5f8a693ac9cf68e5ac9a03f2373b32d77c48904de"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac59294a536ba447b5037f62d8367d7d93b696f80671c2c45645fa9f1109413c"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:086844830fcd1e5c84fec7017ea1ee8491487cfc877847d96f86f68881569d2e"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e42b38fad664f56f77f6fbca22d08450f2464baa68acdbf24841bf900eb98e87"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d08b790a63a9a1cde3b5d7d733ed97d4eb884bfbc92f075a091652d6bfd7709a"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:73ea4cc55e8aea28c86799ecacebca09e5f86500414870a8abaedfcbaf74d288"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:f90938ff137130e47bcec8dc1f4ceb02f10178c766e2ef58a9f657ff1f62d124"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:aa1f13e94b8631c8cd53259250556edcf1de71738936b60febba95750d9632bd"}, - {file = "mmh3-4.1.0-cp39-cp39-win32.whl", hash = "sha256:a3b680b471c181490cf82da2142029edb4298e1bdfcb67c76922dedef789868d"}, - {file = "mmh3-4.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:fefef92e9c544a8dbc08f77a8d1b6d48006a750c4375bbcd5ff8199d761e263b"}, - {file = "mmh3-4.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:8e2c1f6a2b41723a4f82bd5a762a777836d29d664fc0095f17910bea0adfd4a6"}, - {file = "mmh3-4.1.0.tar.gz", hash = "sha256:a1cf25348b9acd229dda464a094d6170f47d2850a1fcb762a3b6172d2ce6ca4a"}, -] - -[package.extras] -test = ["mypy (>=1.0)", "pytest (>=7.0.0)"] - -[[package]] -name = "monotonic" -version = "1.6" -description = "An implementation of time.monotonic() for Python 2 & < 3.3" -optional = false -python-versions = "*" -files = [ - {file = "monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c"}, - {file = "monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7"}, -] - -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] -tests = ["pytest (>=4.6)"] - -[[package]] -name = "msgpack" -version = "1.1.0" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, - {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, - {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, - {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, - {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, - {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, - {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, - {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, - {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, - {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, - {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, - {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, - {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, - {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, -] - -[[package]] -name = "multidict" -version = "6.0.5" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - -[[package]] -name = "mypy" -version = "1.16.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, - {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, - {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"}, - {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"}, - {file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"}, - {file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"}, - {file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"}, - {file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"}, - {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"}, - {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"}, - {file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"}, - {file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"}, - {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, - {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, - {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, - {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, - {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, - {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, - {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, - {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, - {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, - {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, - {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, - {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, - {file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"}, - {file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"}, - {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"}, - {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"}, - {file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"}, - {file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"}, - {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, - {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -description = "Patch asyncio to allow nested event loops" -optional = false -python-versions = ">=3.5" -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "networkx" -version = "3.1" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.8" -files = [ - {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"}, - {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"}, -] - -[package.extras] -default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] -developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"] -doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] -test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[[package]] -name = "nltk" -version = "3.9.1" -description = "Natural Language Toolkit" -optional = false -python-versions = ">=3.8" -files = [ - {file = "nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1"}, - {file = "nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868"}, -] - -[package.dependencies] -click = "*" -joblib = "*" -regex = ">=2021.8.3" -tqdm = "*" - -[package.extras] -all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"] -corenlp = ["requests"] -machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"] -plot = ["matplotlib"] -tgrep = ["pyparsing"] -twitter = ["twython"] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "numpy" -version = "1.26.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, -] - -[[package]] -name = "oauthlib" -version = "3.2.2" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -optional = false -python-versions = ">=3.6" -files = [ - {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, - {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, -] - -[package.extras] -rsa = ["cryptography (>=3.0.0)"] -signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] - -[[package]] -name = "ollama" -version = "0.4.1" -description = "The official Python client for Ollama." -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "ollama-0.4.1-py3-none-any.whl", hash = "sha256:b6fb16aa5a3652633e1716acb12cf2f44aa18beb229329e46a0302734822dfad"}, - {file = "ollama-0.4.1.tar.gz", hash = "sha256:8c6b5e7ff80dd0b8692150b03359f60bac7ca162b088c604069409142a684ad3"}, -] - -[package.dependencies] -httpx = ">=0.27.0,<0.28.0" -pydantic = ">=2.9.0,<3.0.0" - -[[package]] -name = "onnxruntime" -version = "1.17.3" -description = "ONNX Runtime is a runtime accelerator for Machine Learning models" -optional = false -python-versions = "*" -files = [ - {file = "onnxruntime-1.17.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d86dde9c0bb435d709e51bd25991c9fe5b9a5b168df45ce119769edc4d198b15"}, - {file = "onnxruntime-1.17.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d87b68bf931ac527b2d3c094ead66bb4381bac4298b65f46c54fe4d1e255865"}, - {file = "onnxruntime-1.17.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e950cf0333cf114a155f9142e71da344d2b08dfe202763a403ae81cc02ebd1"}, - {file = "onnxruntime-1.17.3-cp310-cp310-win32.whl", hash = "sha256:0962a4d0f5acebf62e1f0bf69b6e0adf16649115d8de854c1460e79972324d68"}, - {file = "onnxruntime-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:468ccb8a0faa25c681a41787b1594bf4448b0252d3efc8b62fd8b2411754340f"}, - {file = "onnxruntime-1.17.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e8cd90c1c17d13d47b89ab076471e07fb85467c01dcd87a8b8b5cdfbcb40aa51"}, - {file = "onnxruntime-1.17.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a058b39801baefe454eeb8acf3ada298c55a06a4896fafc224c02d79e9037f60"}, - {file = "onnxruntime-1.17.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f823d5eb4807007f3da7b27ca972263df6a1836e6f327384eb266274c53d05d"}, - {file = "onnxruntime-1.17.3-cp311-cp311-win32.whl", hash = "sha256:b66b23f9109e78ff2791628627a26f65cd335dcc5fbd67ff60162733a2f7aded"}, - {file = "onnxruntime-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:570760ca53a74cdd751ee49f13de70d1384dcf73d9888b8deac0917023ccda6d"}, - {file = "onnxruntime-1.17.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:77c318178d9c16e9beadd9a4070d8aaa9f57382c3f509b01709f0f010e583b99"}, - {file = "onnxruntime-1.17.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23da8469049b9759082e22c41a444f44a520a9c874b084711b6343672879f50b"}, - {file = "onnxruntime-1.17.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2949730215af3f9289008b2e31e9bbef952012a77035b911c4977edea06f3f9e"}, - {file = "onnxruntime-1.17.3-cp312-cp312-win32.whl", hash = "sha256:6c7555a49008f403fb3b19204671efb94187c5085976ae526cb625f6ede317bc"}, - {file = "onnxruntime-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:58672cf20293a1b8a277a5c6c55383359fcdf6119b2f14df6ce3b140f5001c39"}, - {file = "onnxruntime-1.17.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4395ba86e3c1e93c794a00619ef1aec597ab78f5a5039f3c6d2e9d0695c0a734"}, - {file = "onnxruntime-1.17.3-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf354c04344ec38564fc22394e1fe08aa6d70d790df00159205a0055c4a4d3f"}, - {file = "onnxruntime-1.17.3-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a94b600b7af50e922d44b95a57981e3e35103c6e3693241a03d3ca204740bbda"}, - {file = "onnxruntime-1.17.3-cp38-cp38-win32.whl", hash = "sha256:5a335c76f9c002a8586c7f38bc20fe4b3725ced21f8ead835c3e4e507e42b2ab"}, - {file = "onnxruntime-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f56a86fbd0ddc8f22696ddeda0677b041381f4168a2ca06f712ef6ec6050d6d"}, - {file = "onnxruntime-1.17.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:e0ae39f5452278cd349520c296e7de3e90d62dc5b0157c6868e2748d7f28b871"}, - {file = "onnxruntime-1.17.3-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ff2dc012bd930578aff5232afd2905bf16620815f36783a941aafabf94b3702"}, - {file = "onnxruntime-1.17.3-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf6c37483782e4785019b56e26224a25e9b9a35b849d0169ce69189867a22bb1"}, - {file = "onnxruntime-1.17.3-cp39-cp39-win32.whl", hash = "sha256:351bf5a1140dcc43bfb8d3d1a230928ee61fcd54b0ea664c8e9a889a8e3aa515"}, - {file = "onnxruntime-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:57a3de15778da8d6cc43fbf6cf038e1e746146300b5f0b1fbf01f6f795dc6440"}, -] - -[package.dependencies] -coloredlogs = "*" -flatbuffers = "*" -numpy = ">=1.21.6" -packaging = "*" -protobuf = "*" -sympy = "*" - -[[package]] -name = "openai" -version = "1.93.0" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "openai-1.93.0-py3-none-any.whl", hash = "sha256:3d746fe5498f0dd72e0d9ab706f26c91c0f646bf7459e5629af8ba7c9dbdf090"}, - {file = "openai-1.93.0.tar.gz", hash = "sha256:988f31ade95e1ff0585af11cc5a64510225e4f5cd392698c675d0a9265b8e337"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.11,<5" - -[package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.6)"] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -realtime = ["websockets (>=13,<16)"] -voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] - -[[package]] -name = "opentelemetry-api" -version = "1.33.1" -description = "OpenTelemetry Python API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_api-1.33.1-py3-none-any.whl", hash = "sha256:4db83ebcf7ea93e64637ec6ee6fabee45c5cbe4abd9cf3da95c43828ddb50b83"}, - {file = "opentelemetry_api-1.33.1.tar.gz", hash = "sha256:1c6055fc0a2d3f23a50c7e17e16ef75ad489345fd3df1f8b8af7c0bbf8a109e8"}, -] - -[package.dependencies] -deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<8.7.0" - -[[package]] -name = "opentelemetry-exporter-otlp" -version = "1.33.1" -description = "OpenTelemetry Collector Exporters" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_exporter_otlp-1.33.1-py3-none-any.whl", hash = "sha256:9bcf1def35b880b55a49e31ebd63910edac14b294fd2ab884953c4deaff5b300"}, - {file = "opentelemetry_exporter_otlp-1.33.1.tar.gz", hash = "sha256:4d050311ea9486e3994575aa237e32932aad58330a31fba24fdba5c0d531cf04"}, -] - -[package.dependencies] -opentelemetry-exporter-otlp-proto-grpc = "1.33.1" -opentelemetry-exporter-otlp-proto-http = "1.33.1" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.33.1" -description = "OpenTelemetry Protobuf encoding" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.33.1-py3-none-any.whl", hash = "sha256:b81c1de1ad349785e601d02715b2d29d6818aed2c809c20219f3d1f20b038c36"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.33.1.tar.gz", hash = "sha256:c57b3fa2d0595a21c4ed586f74f948d259d9949b58258f11edb398f246bec131"}, -] - -[package.dependencies] -opentelemetry-proto = "1.33.1" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.33.1" -description = "OpenTelemetry Collector Protobuf over gRPC Exporter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.33.1-py3-none-any.whl", hash = "sha256:7e8da32c7552b756e75b4f9e9c768a61eb47dee60b6550b37af541858d669ce1"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.33.1.tar.gz", hash = "sha256:345696af8dc19785fac268c8063f3dc3d5e274c774b308c634f39d9c21955728"}, -] - -[package.dependencies] -deprecated = ">=1.2.6" -googleapis-common-protos = ">=1.52,<2.0" -grpcio = [ - {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, - {version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""}, -] -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.33.1" -opentelemetry-proto = "1.33.1" -opentelemetry-sdk = ">=1.33.1,<1.34.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.33.1" -description = "OpenTelemetry Collector Protobuf over HTTP Exporter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_exporter_otlp_proto_http-1.33.1-py3-none-any.whl", hash = "sha256:ebd6c523b89a2ecba0549adb92537cc2bf647b4ee61afbbd5a4c6535aa3da7cf"}, - {file = "opentelemetry_exporter_otlp_proto_http-1.33.1.tar.gz", hash = "sha256:46622d964a441acb46f463ebdc26929d9dec9efb2e54ef06acdc7305e8593c38"}, -] - -[package.dependencies] -deprecated = ">=1.2.6" -googleapis-common-protos = ">=1.52,<2.0" -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.33.1" -opentelemetry-proto = "1.33.1" -opentelemetry-sdk = ">=1.33.1,<1.34.0" -requests = ">=2.7,<3.0" - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.54b1" -description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_instrumentation-0.54b1-py3-none-any.whl", hash = "sha256:a4ae45f4a90c78d7006c51524f57cd5aa1231aef031eae905ee34d5423f5b198"}, - {file = "opentelemetry_instrumentation-0.54b1.tar.gz", hash = "sha256:7658bf2ff914b02f246ec14779b66671508125c0e4227361e56b5ebf6cef0aec"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.4,<2.0" -opentelemetry-semantic-conventions = "0.54b1" -packaging = ">=18.0" -wrapt = ">=1.0.0,<2.0.0" - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.54b1" -description = "ASGI instrumentation for OpenTelemetry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_instrumentation_asgi-0.54b1-py3-none-any.whl", hash = "sha256:84674e822b89af563b283a5283c2ebb9ed585d1b80a1c27fb3ac20b562e9f9fc"}, - {file = "opentelemetry_instrumentation_asgi-0.54b1.tar.gz", hash = "sha256:ab4df9776b5f6d56a78413c2e8bbe44c90694c67c844a1297865dc1bd926ed3c"}, -] - -[package.dependencies] -asgiref = ">=3.0,<4.0" -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.54b1" -opentelemetry-semantic-conventions = "0.54b1" -opentelemetry-util-http = "0.54b1" - -[package.extras] -instruments = ["asgiref (>=3.0,<4.0)"] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.54b1" -description = "OpenTelemetry FastAPI Instrumentation" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_instrumentation_fastapi-0.54b1-py3-none-any.whl", hash = "sha256:fb247781cfa75fd09d3d8713c65e4a02bd1e869b00e2c322cc516d4b5429860c"}, - {file = "opentelemetry_instrumentation_fastapi-0.54b1.tar.gz", hash = "sha256:1fcad19cef0db7092339b571a59e6f3045c9b58b7fd4670183f7addc459d78df"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.54b1" -opentelemetry-instrumentation-asgi = "0.54b1" -opentelemetry-semantic-conventions = "0.54b1" -opentelemetry-util-http = "0.54b1" - -[package.extras] -instruments = ["fastapi (>=0.58,<1.0)"] - -[[package]] -name = "opentelemetry-proto" -version = "1.33.1" -description = "OpenTelemetry Python Proto" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_proto-1.33.1-py3-none-any.whl", hash = "sha256:243d285d9f29663fc7ea91a7171fcc1ccbbfff43b48df0774fd64a37d98eda70"}, - {file = "opentelemetry_proto-1.33.1.tar.gz", hash = "sha256:9627b0a5c90753bf3920c398908307063e4458b287bb890e5c1d6fa11ad50b68"}, -] - -[package.dependencies] -protobuf = ">=5.0,<6.0" - -[[package]] -name = "opentelemetry-sdk" -version = "1.33.1" -description = "OpenTelemetry Python SDK" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_sdk-1.33.1-py3-none-any.whl", hash = "sha256:19ea73d9a01be29cacaa5d6c8ce0adc0b7f7b4d58cc52f923e4413609f670112"}, - {file = "opentelemetry_sdk-1.33.1.tar.gz", hash = "sha256:85b9fcf7c3d23506fbc9692fd210b8b025a1920535feec50bd54ce203d57a531"}, -] - -[package.dependencies] -opentelemetry-api = "1.33.1" -opentelemetry-semantic-conventions = "0.54b1" -typing-extensions = ">=3.7.4" - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.54b1" -description = "OpenTelemetry Semantic Conventions" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_semantic_conventions-0.54b1-py3-none-any.whl", hash = "sha256:29dab644a7e435b58d3a3918b58c333c92686236b30f7891d5e51f02933ca60d"}, - {file = "opentelemetry_semantic_conventions-0.54b1.tar.gz", hash = "sha256:d1cecedae15d19bdaafca1e56b29a66aa286f50b5d08f036a145c7f3e9ef9cee"}, -] - -[package.dependencies] -deprecated = ">=1.2.6" -opentelemetry-api = "1.33.1" - -[[package]] -name = "opentelemetry-util-http" -version = "0.54b1" -description = "Web util for OpenTelemetry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_util_http-0.54b1-py3-none-any.whl", hash = "sha256:b1c91883f980344a1c3c486cffd47ae5c9c1dd7323f9cbe9fdb7cadb401c87c9"}, - {file = "opentelemetry_util_http-0.54b1.tar.gz", hash = "sha256:f0b66868c19fbaf9c9d4e11f4a7599fa15d5ea50b884967a26ccd9d72c7c9d15"}, -] - -[[package]] -name = "orjson" -version = "3.10.2" -description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "orjson-3.10.2-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:87124c1b3471a072fda422e156dd7ef086d854937d68adc266f17f32a1043c95"}, - {file = "orjson-3.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1b79526bd039e775ad0f558800c3cd9f3bde878a1268845f63984d37bcbb5d1"}, - {file = "orjson-3.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f6dc97a6b2833a0d77598e7d016b6d964e4b0bc9576c89aa9a16fcf8ac902d"}, - {file = "orjson-3.10.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e427ce004fe15e13dcfdbd6c9dc936abf83d85d2164ec415a8bd90954f6f781"}, - {file = "orjson-3.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f3e05f70ab6225ba38504a2be61935d6ebc09de2b1bc484c30cb96ca4fa24b8"}, - {file = "orjson-3.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4e67821e3c1f0ec5dbef9dbd0bc9cd0fe4f0d8ba5d76a07038ee3843c9ac98a"}, - {file = "orjson-3.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24877561fe96a3736224243d6e2e026a674a4ddeff2b02fdeac41801bd261c87"}, - {file = "orjson-3.10.2-cp310-none-win32.whl", hash = "sha256:5da4ce52892b00aa51f5c5781414dc2bcdecc8470d2d60eeaeadbc14c5d9540b"}, - {file = "orjson-3.10.2-cp310-none-win_amd64.whl", hash = "sha256:cee3df171d957e84f568c3920f1f077f7f2a69f8ce4303d4c1404b7aab2f365a"}, - {file = "orjson-3.10.2-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a361e7ad84452416a469cdda7a2efeee8ddc9e06e4b95938b072045e205f86dc"}, - {file = "orjson-3.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b064251af6a2b7fb26e51b9abd3c1e615b53d5d5f87972263233d66d9c736a4"}, - {file = "orjson-3.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:464c30c24961cc83b2dc0e5532ed41084624ee1c71d4e7ef1aaec88f7a677393"}, - {file = "orjson-3.10.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4459005982748fda9871f04bce6a304c515afc46c96bef51e2bc81755c0f4ea0"}, - {file = "orjson-3.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abd0cd3a113a6ea0051c4a50cca65161ee50c014a01363554a1417d9f3c4529f"}, - {file = "orjson-3.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9a658ebc5143fbc0a9e3a10aafce4de50b01b1b0a41942038cb4bc6617f1e1d7"}, - {file = "orjson-3.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2fa4addaf6a6b3eb836cf92c4986d5ef9215fbdc87e4891cf8fd97990972bba0"}, - {file = "orjson-3.10.2-cp311-none-win32.whl", hash = "sha256:faff04363bfcff9cb41ab09c0ce8db84b8d4a09a374305ec5b12210dfa3154ea"}, - {file = "orjson-3.10.2-cp311-none-win_amd64.whl", hash = "sha256:7aee7b31a6acecf65a94beef2191081692891b00e8b7e02fbcc0c85002d62d0b"}, - {file = "orjson-3.10.2-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:38d9e9eab01131fdccbe95bff4f1d8ea197d239b5c73396e2079d07730bfa205"}, - {file = "orjson-3.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfd84ecf5ebe8ec334a95950427e7ade40135032b1f00e2b17f351b0ef6dc72b"}, - {file = "orjson-3.10.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2ba009d85c3c98006759e62150d018d622aa79012fdeefbb70a42a542582b45"}, - {file = "orjson-3.10.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eac25b54fab6d9ccbf9dbc57555c2b52bf6d0802ea84bd2bd9670a161bd881dc"}, - {file = "orjson-3.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e735d90a90caf746de59becf29642c8358cafcd9b1a906ae3566efcc495324"}, - {file = "orjson-3.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:12feeee9089654904c2c988788eb9d521f5752c83ea410969d1f58d05ea95943"}, - {file = "orjson-3.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:619a7a4df76497afd2e6f1c963cc7e13658b3d58425c3a2ccf0471ad61d71025"}, - {file = "orjson-3.10.2-cp312-none-win32.whl", hash = "sha256:460d221090b451a0e78813196ec9dd28d2e33103048cfd7c1a3312a532fe3b1f"}, - {file = "orjson-3.10.2-cp312-none-win_amd64.whl", hash = "sha256:7efa93a9540e6ac9fe01167389fd7b1f0250cbfe3a8f06fe23e045d2a2d5d6ac"}, - {file = "orjson-3.10.2-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ceb283b8c048fb20bd1c703b10e710783a4f1ba7d5654358a25db99e9df94d5"}, - {file = "orjson-3.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201bf2b96ba39941254ef6b02e080660861e1444ec50be55778e1c38446c2d39"}, - {file = "orjson-3.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51a7b67c8cddf1a9de72d534244590103b1f17b2105d3bdcb221981bd97ab427"}, - {file = "orjson-3.10.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cde123c227e28ef9bba7092dc88abbd1933a0d7c17c58970c8ed8ec804e7add5"}, - {file = "orjson-3.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b51caf8720b6df448acf764312d4678aeed6852ebfa6f3aa28b6061155ffef"}, - {file = "orjson-3.10.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f124d7e813e7b3d56bb7841d3d0884fec633f5f889a27a158d004b6b37e5ca98"}, - {file = "orjson-3.10.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e33ac7a6b081688a2167b501c9813aa6ec1f2cc097c47ab5f33cca3e875da9dc"}, - {file = "orjson-3.10.2-cp38-none-win32.whl", hash = "sha256:8f4a91921270d646f50f90a9903f87baae24c6e376ef3c275fcd0ffc051117bb"}, - {file = "orjson-3.10.2-cp38-none-win_amd64.whl", hash = "sha256:148d266e300257ff6d8e8a5895cc1e12766b8db676510b4f1d79b0d07f666fdd"}, - {file = "orjson-3.10.2-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:27158a75e7239145cf385d2318fdb27fbcd1fc494a470ee68287147c8b214cb1"}, - {file = "orjson-3.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26302b13e3f542b3e1ad1723e3543caf28e2f372391d21e1642de29c06e6209"}, - {file = "orjson-3.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:712cb3aa976311ae53de116a64949392aa5e7dcceda6769d5d7169d303d5ed09"}, - {file = "orjson-3.10.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9db3e6f23a6c9ce6c883a8e10e0eae0e2895327fb6e2286019b13153e59c672f"}, - {file = "orjson-3.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44787769d93d1ef9f25a80644ef020e0f30f37045d6336133e421a414c8fe51"}, - {file = "orjson-3.10.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:53a43b18d280c8d18cb18437921a05ec478b908809f9e89ad60eb2fdf0ba96ac"}, - {file = "orjson-3.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99e270b6a13027ed4c26c2b75b06c2cfb950934c8eb0400d70f4e6919bfe24f4"}, - {file = "orjson-3.10.2-cp39-none-win32.whl", hash = "sha256:d6f71486d211db9a01094cdd619ab594156a43ca04fa24e23ee04dac1509cdca"}, - {file = "orjson-3.10.2-cp39-none-win_amd64.whl", hash = "sha256:161f3b4e6364132562af80967ac3211e6681d320a01954da4915af579caab0b2"}, - {file = "orjson-3.10.2.tar.gz", hash = "sha256:47affe9f704c23e49a0fbb9d441af41f602474721e8639e8814640198f9ae32f"}, -] - -[[package]] -name = "overrides" -version = "7.7.0" -description = "A decorator to automatically detect mismatch when overriding a method." -optional = false -python-versions = ">=3.6" -files = [ - {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, - {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, -] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pandas" -version = "2.0.3" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, - {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, - {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, - {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, - {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, - {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, - {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, - {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, - {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, - {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, - {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] -aws = ["s3fs (>=2021.08.0)"] -clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] -compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] -computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2021.07.0)"] -gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] -hdf5 = ["tables (>=3.6.1)"] -html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] -mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] -spss = ["pyreadstat (>=1.1.2)"] -sql-other = ["SQLAlchemy (>=1.4.16)"] -test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.6.3)"] - -[[package]] -name = "parameterized" -version = "0.9.0" -description = "Parameterized testing with any Python test framework" -optional = false -python-versions = ">=3.7" -files = [ - {file = "parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b"}, - {file = "parameterized-0.9.0.tar.gz", hash = "sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1"}, -] - -[package.extras] -dev = ["jinja2"] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pdoc" -version = "14.7.0" -description = "API Documentation for Python Projects" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pdoc-14.7.0-py3-none-any.whl", hash = "sha256:72377a907efc6b2c5b3c56b717ef34f11d93621dced3b663f3aede0b844c0ad2"}, - {file = "pdoc-14.7.0.tar.gz", hash = "sha256:2d28af9c0acc39180744ad0543e4bbc3223ecba0d1302db315ec521c51f71f93"}, -] - -[package.dependencies] -Jinja2 = ">=2.11.0" -MarkupSafe = "*" -pygments = ">=2.12.0" - -[package.extras] -dev = ["hypothesis", "mypy", "pdoc-pyo3-sample-library (==1.0.11)", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"] - -[[package]] -name = "pillow" -version = "10.4.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, - {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, - {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, - {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, - {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, - {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, - {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, - {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, - {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, - {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, - {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, - {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, - {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, - {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, - {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, - {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, - {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, - {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, - {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, - {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] -xmp = ["defusedxml"] - -[[package]] -name = "platformdirs" -version = "4.2.1" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "posthog" -version = "3.5.0" -description = "Integrate PostHog into any python application." -optional = false -python-versions = "*" -files = [ - {file = "posthog-3.5.0-py2.py3-none-any.whl", hash = "sha256:3c672be7ba6f95d555ea207d4486c171d06657eb34b3ce25eb043bfe7b6b5b76"}, - {file = "posthog-3.5.0.tar.gz", hash = "sha256:8f7e3b2c6e8714d0c0c542a2109b83a7549f63b7113a133ab2763a89245ef2ef"}, -] - -[package.dependencies] -backoff = ">=1.10.0" -monotonic = ">=1.5" -python-dateutil = ">2.1" -requests = ">=2.7,<3.0" -six = ">=1.5" - -[package.extras] -dev = ["black", "flake8", "flake8-print", "isort", "pre-commit"] -sentry = ["django", "sentry-sdk"] -test = ["coverage", "flake8", "freezegun (==0.3.15)", "mock (>=2.0.0)", "pylint", "pytest", "pytest-timeout"] - -[[package]] -name = "pre-commit" -version = "3.5.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "propcache" -version = "0.2.0" -description = "Accelerated property cache" -optional = false -python-versions = ">=3.8" -files = [ - {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, - {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, - {file = "propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336"}, - {file = "propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad"}, - {file = "propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99"}, - {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, - {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, - {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, - {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, - {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, - {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2"}, - {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7"}, - {file = "propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348"}, - {file = "propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5"}, - {file = "propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3"}, - {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7"}, - {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763"}, - {file = "propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544"}, - {file = "propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032"}, - {file = "propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e"}, - {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861"}, - {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6"}, - {file = "propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed"}, - {file = "propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d"}, - {file = "propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5"}, - {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6"}, - {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638"}, - {file = "propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798"}, - {file = "propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9"}, - {file = "propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df"}, - {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, - {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, -] - -[[package]] -name = "proto-plus" -version = "1.26.1" -description = "Beautiful, Pythonic protocol buffers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, - {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, -] - -[package.dependencies] -protobuf = ">=3.19.0,<7.0.0" - -[package.extras] -testing = ["google-api-core (>=1.31.5)"] - -[[package]] -name = "protobuf" -version = "5.29.5" -description = "" -optional = false -python-versions = ">=3.8" -files = [ - {file = "protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079"}, - {file = "protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc"}, - {file = "protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671"}, - {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015"}, - {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61"}, - {file = "protobuf-5.29.5-cp38-cp38-win32.whl", hash = "sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238"}, - {file = "protobuf-5.29.5-cp38-cp38-win_amd64.whl", hash = "sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e"}, - {file = "protobuf-5.29.5-cp39-cp39-win32.whl", hash = "sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736"}, - {file = "protobuf-5.29.5-cp39-cp39-win_amd64.whl", hash = "sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353"}, - {file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"}, - {file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"}, -] - -[[package]] -name = "pyasn1" -version = "0.6.0" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, - {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.0" -description = "A collection of ASN.1-based protocols modules" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, - {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, -] - -[package.dependencies] -pyasn1 = ">=0.4.6,<0.7.0" - -[[package]] -name = "pydantic" -version = "2.9.2" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, - {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.23.4" -typing-extensions = [ - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, -] - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.23.4" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, - {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, - {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, - {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, - {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, - {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, - {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, - {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, - {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, - {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, - {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, - {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, - {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, - {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydantic-settings" -version = "2.6.1" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"}, - {file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"}, -] - -[package.dependencies] -pydantic = ">=2.7.0" -python-dotenv = ">=0.21.0" - -[package.extras] -azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pygments" -version = "2.17.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, -] - -[package.extras] -plugins = ["importlib-metadata"] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pymongo" -version = "4.7.3" -description = "Python driver for MongoDB " -optional = false -python-versions = ">=3.7" -files = [ - {file = "pymongo-4.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e9580b4537b3cc5d412070caabd1dabdf73fdce249793598792bac5782ecf2eb"}, - {file = "pymongo-4.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:517243b2b189c98004570dd8fc0e89b1a48363d5578b3b99212fa2098b2ea4b8"}, - {file = "pymongo-4.7.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23b1e9dabd61da1c7deb54d888f952f030e9e35046cebe89309b28223345b3d9"}, - {file = "pymongo-4.7.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03e0f9901ad66c6fb7da0d303461377524d61dab93a4e4e5af44164c5bb4db76"}, - {file = "pymongo-4.7.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a870824aa54453aee030bac08c77ebcf2fe8999400f0c2a065bebcbcd46b7f8"}, - {file = "pymongo-4.7.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd7b3d3f4261bddbb74a332d87581bc523353e62bb9da4027cc7340f6fcbebc"}, - {file = "pymongo-4.7.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d719a643ea6da46d215a3ba51dac805a773b611c641319558d8576cbe31cef8"}, - {file = "pymongo-4.7.3-cp310-cp310-win32.whl", hash = "sha256:d8b1e06f361f3c66ee694cb44326e1a2e4f93bc9c3a4849ae8547889fca71154"}, - {file = "pymongo-4.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:c450ab2f9397e2d5caa7fddeb4feb30bf719c47c13ae02c0bbb3b71bf4099c1c"}, - {file = "pymongo-4.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cc6459209e885ba097779eaa0fe7f2fa049db39ab43b1731cf8d065a4650e8"}, - {file = "pymongo-4.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e2287f1e2cc35e73cd74a4867e398a97962c5578a3991c730ef78d276ca8e46"}, - {file = "pymongo-4.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:413506bd48d8c31ee100645192171e4773550d7cb940b594d5175ac29e329ea1"}, - {file = "pymongo-4.7.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cc1febf17646d52b7561caa762f60bdfe2cbdf3f3e70772f62eb624269f9c05"}, - {file = "pymongo-4.7.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dfcf18a49955d50a16c92b39230bd0668ffc9c164ccdfe9d28805182b48fa72"}, - {file = "pymongo-4.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89872041196c008caddf905eb59d3dc2d292ae6b0282f1138418e76f3abd3ad6"}, - {file = "pymongo-4.7.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3ed97b89de62ea927b672ad524de0d23f3a6b4a01c8d10e3d224abec973fbc3"}, - {file = "pymongo-4.7.3-cp311-cp311-win32.whl", hash = "sha256:d2f52b38151e946011d888a8441d3d75715c663fc5b41a7ade595e924e12a90a"}, - {file = "pymongo-4.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:4a4cc91c28e81c0ce03d3c278e399311b0af44665668a91828aec16527082676"}, - {file = "pymongo-4.7.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cb30c8a78f5ebaca98640943447b6a0afcb146f40b415757c9047bf4a40d07b4"}, - {file = "pymongo-4.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9cf2069f5d37c398186453589486ea98bb0312214c439f7d320593b61880dc05"}, - {file = "pymongo-4.7.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3564f423958fced8a8c90940fd2f543c27adbcd6c7c6ed6715d847053f6200a0"}, - {file = "pymongo-4.7.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a8af8a38fa6951fff73e6ff955a6188f829b29fed7c5a1b739a306b4aa56fe8"}, - {file = "pymongo-4.7.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a0e81c8dba6d825272867d487f18764cfed3c736d71d7d4ff5b79642acbed42"}, - {file = "pymongo-4.7.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88fc1d146feabac4385ea8ddb1323e584922922641303c8bf392fe1c36803463"}, - {file = "pymongo-4.7.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4225100b2c5d1f7393d7c5d256ceb8b20766830eecf869f8ae232776347625a6"}, - {file = "pymongo-4.7.3-cp312-cp312-win32.whl", hash = "sha256:5f3569ed119bf99c0f39ac9962fb5591eff02ca210fe80bb5178d7a1171c1b1e"}, - {file = "pymongo-4.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:eb383c54c0c8ba27e7712b954fcf2a0905fee82a929d277e2e94ad3a5ba3c7db"}, - {file = "pymongo-4.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a46cffe91912570151617d866a25d07b9539433a32231ca7e7cf809b6ba1745f"}, - {file = "pymongo-4.7.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c3cba427dac50944c050c96d958c5e643c33a457acee03bae27c8990c5b9c16"}, - {file = "pymongo-4.7.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a5fd893edbeb7fa982f8d44b6dd0186b6cd86c89e23f6ef95049ff72bffe46"}, - {file = "pymongo-4.7.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c168a2fadc8b19071d0a9a4f85fe38f3029fe22163db04b4d5c046041c0b14bd"}, - {file = "pymongo-4.7.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c59c2c9e70f63a7f18a31e367898248c39c068c639b0579623776f637e8f482"}, - {file = "pymongo-4.7.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08165fd82c89d372e82904c3268bd8fe5de44f92a00e97bb1db1785154397d9"}, - {file = "pymongo-4.7.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:397fed21afec4fdaecf72f9c4344b692e489756030a9c6d864393e00c7e80491"}, - {file = "pymongo-4.7.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f903075f8625e2d228f1b9b9a0cf1385f1c41e93c03fd7536c91780a0fb2e98f"}, - {file = "pymongo-4.7.3-cp37-cp37m-win32.whl", hash = "sha256:8ed1132f58c38add6b6138b771d0477a3833023c015c455d9a6e26f367f9eb5c"}, - {file = "pymongo-4.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8d00a5d8fc1043a4f641cbb321da766699393f1b6f87c70fae8089d61c9c9c54"}, - {file = "pymongo-4.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9377b868c38700c7557aac1bc4baae29f47f1d279cc76b60436e547fd643318c"}, - {file = "pymongo-4.7.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da4a6a7b4f45329bb135aa5096823637bd5f760b44d6224f98190ee367b6b5dd"}, - {file = "pymongo-4.7.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:487e2f9277f8a63ac89335ec4f1699ae0d96ebd06d239480d69ed25473a71b2c"}, - {file = "pymongo-4.7.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db3d608d541a444c84f0bfc7bad80b0b897e0f4afa580a53f9a944065d9b633"}, - {file = "pymongo-4.7.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e90af2ad3a8a7c295f4d09a2fbcb9a350c76d6865f787c07fe843b79c6e821d1"}, - {file = "pymongo-4.7.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e28feb18dc559d50ededba27f9054c79f80c4edd70a826cecfe68f3266807b3"}, - {file = "pymongo-4.7.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f21ecddcba2d9132d5aebd8e959de8d318c29892d0718420447baf2b9bccbb19"}, - {file = "pymongo-4.7.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:26140fbb3f6a9a74bd73ed46d0b1f43d5702e87a6e453a31b24fad9c19df9358"}, - {file = "pymongo-4.7.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:94baa5fc7f7d22c3ce2ac7bd92f7e03ba7a6875f2480e3b97a400163d6eaafc9"}, - {file = "pymongo-4.7.3-cp38-cp38-win32.whl", hash = "sha256:92dd247727dd83d1903e495acc743ebd757f030177df289e3ba4ef8a8c561fad"}, - {file = "pymongo-4.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:1c90c848a5e45475731c35097f43026b88ef14a771dfd08f20b67adc160a3f79"}, - {file = "pymongo-4.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f598be401b416319a535c386ac84f51df38663f7a9d1071922bda4d491564422"}, - {file = "pymongo-4.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:35ba90477fae61c65def6e7d09e8040edfdd3b7fd47c3c258b4edded60c4d625"}, - {file = "pymongo-4.7.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aa8735955c70892634d7e61b0ede9b1eefffd3cd09ccabee0ffcf1bdfe62254"}, - {file = "pymongo-4.7.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82a97d8f7f138586d9d0a0cff804a045cdbbfcfc1cd6bba542b151e284fbbec5"}, - {file = "pymongo-4.7.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de3b9db558930efab5eaef4db46dcad8bf61ac3ddfd5751b3e5ac6084a25e366"}, - {file = "pymongo-4.7.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0e149217ef62812d3c2401cf0e2852b0c57fd155297ecc4dcd67172c4eca402"}, - {file = "pymongo-4.7.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3a8a1ef4a824f5feb793b3231526d0045eadb5eb01080e38435dfc40a26c3e5"}, - {file = "pymongo-4.7.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d14e5e89a4be1f10efc3d9dcb13eb7a3b2334599cb6bb5d06c6a9281b79c8e22"}, - {file = "pymongo-4.7.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6bfa29f032fd4fd7b129520f8cdb51ab71d88c2ba0567cccd05d325f963acb5"}, - {file = "pymongo-4.7.3-cp39-cp39-win32.whl", hash = "sha256:1421d0bd2ce629405f5157bd1aaa9b83f12d53a207cf68a43334f4e4ee312b66"}, - {file = "pymongo-4.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:f7ee974f8b9370a998919c55b1050889f43815ab588890212023fecbc0402a6d"}, - {file = "pymongo-4.7.3.tar.gz", hash = "sha256:6354a66b228f2cd399be7429685fb68e07f19110a3679782ecb4fdb68da03831"}, -] - -[package.dependencies] -dnspython = ">=1.16.0,<3.0.0" - -[package.extras] -aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] -encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.6.0,<2.0.0)"] -gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] -ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] -snappy = ["python-snappy"] -test = ["pytest (>=7)"] -zstd = ["zstandard"] - -[[package]] -name = "pypika" -version = "0.48.9" -description = "A SQL query builder API for Python" -optional = false -python-versions = "*" -files = [ - {file = "PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378"}, -] - -[[package]] -name = "pyproject-hooks" -version = "1.1.0" -description = "Wrappers to call pyproject.toml-based build backend hooks." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2"}, - {file = "pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965"}, -] - -[[package]] -name = "pyreadline3" -version = "3.4.1" -description = "A python implementation of GNU readline." -optional = false -python-versions = "*" -files = [ - {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, - {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, -] - -[[package]] -name = "pytest" -version = "8.3.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.23.8" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, -] - -[package.dependencies] -pytest = ">=7.0.0,<9" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-httpserver" -version = "1.0.12" -description = "pytest-httpserver is a httpserver for pytest" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_httpserver-1.0.12-py3-none-any.whl", hash = "sha256:dae1c79ec7aeda83bfaaf4d0a400867a4b1bc6bf668244daaf13aa814e3022da"}, - {file = "pytest_httpserver-1.0.12.tar.gz", hash = "sha256:c14600b8efb9ea8d7e63251a242ab987f13028b36d3d397ffaca3c929f67eb16"}, -] - -[package.dependencies] -Werkzeug = ">=2.0.0" - -[[package]] -name = "pytest-timeout" -version = "2.3.1" -description = "pytest plugin to abort hanging tests" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, - {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pytz" -version = "2024.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "regex" -version = "2024.4.28" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.8" -files = [ - {file = "regex-2024.4.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd196d056b40af073d95a2879678585f0b74ad35190fac04ca67954c582c6b61"}, - {file = "regex-2024.4.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8bb381f777351bd534462f63e1c6afb10a7caa9fa2a421ae22c26e796fe31b1f"}, - {file = "regex-2024.4.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:47af45b6153522733aa6e92543938e97a70ce0900649ba626cf5aad290b737b6"}, - {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d6a550425cc51c656331af0e2b1651e90eaaa23fb4acde577cf15068e2e20f"}, - {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf29304a8011feb58913c382902fde3395957a47645bf848eea695839aa101b7"}, - {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92da587eee39a52c91aebea8b850e4e4f095fe5928d415cb7ed656b3460ae79a"}, - {file = "regex-2024.4.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6277d426e2f31bdbacb377d17a7475e32b2d7d1f02faaecc48d8e370c6a3ff31"}, - {file = "regex-2024.4.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28e1f28d07220c0f3da0e8fcd5a115bbb53f8b55cecf9bec0c946eb9a059a94c"}, - {file = "regex-2024.4.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aaa179975a64790c1f2701ac562b5eeb733946eeb036b5bcca05c8d928a62f10"}, - {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6f435946b7bf7a1b438b4e6b149b947c837cb23c704e780c19ba3e6855dbbdd3"}, - {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:19d6c11bf35a6ad077eb23852827f91c804eeb71ecb85db4ee1386825b9dc4db"}, - {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:fdae0120cddc839eb8e3c15faa8ad541cc6d906d3eb24d82fb041cfe2807bc1e"}, - {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e672cf9caaf669053121f1766d659a8813bd547edef6e009205378faf45c67b8"}, - {file = "regex-2024.4.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f57515750d07e14743db55d59759893fdb21d2668f39e549a7d6cad5d70f9fea"}, - {file = "regex-2024.4.28-cp310-cp310-win32.whl", hash = "sha256:a1409c4eccb6981c7baabc8888d3550df518add6e06fe74fa1d9312c1838652d"}, - {file = "regex-2024.4.28-cp310-cp310-win_amd64.whl", hash = "sha256:1f687a28640f763f23f8a9801fe9e1b37338bb1ca5d564ddd41619458f1f22d1"}, - {file = "regex-2024.4.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84077821c85f222362b72fdc44f7a3a13587a013a45cf14534df1cbbdc9a6796"}, - {file = "regex-2024.4.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45d4503de8f4f3dc02f1d28a9b039e5504a02cc18906cfe744c11def942e9eb"}, - {file = "regex-2024.4.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:457c2cd5a646dd4ed536c92b535d73548fb8e216ebee602aa9f48e068fc393f3"}, - {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b51739ddfd013c6f657b55a508de8b9ea78b56d22b236052c3a85a675102dc6"}, - {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:459226445c7d7454981c4c0ce0ad1a72e1e751c3e417f305722bbcee6697e06a"}, - {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:670fa596984b08a4a769491cbdf22350431970d0112e03d7e4eeaecaafcd0fec"}, - {file = "regex-2024.4.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe00f4fe11c8a521b173e6324d862ee7ee3412bf7107570c9b564fe1119b56fb"}, - {file = "regex-2024.4.28-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36f392dc7763fe7924575475736bddf9ab9f7a66b920932d0ea50c2ded2f5636"}, - {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:23a412b7b1a7063f81a742463f38821097b6a37ce1e5b89dd8e871d14dbfd86b"}, - {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f1d6e4b7b2ae3a6a9df53efbf199e4bfcff0959dbdb5fd9ced34d4407348e39a"}, - {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:499334ad139557de97cbc4347ee921c0e2b5e9c0f009859e74f3f77918339257"}, - {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:0940038bec2fe9e26b203d636c44d31dd8766abc1fe66262da6484bd82461ccf"}, - {file = "regex-2024.4.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:66372c2a01782c5fe8e04bff4a2a0121a9897e19223d9eab30c54c50b2ebeb7f"}, - {file = "regex-2024.4.28-cp311-cp311-win32.whl", hash = "sha256:c77d10ec3c1cf328b2f501ca32583625987ea0f23a0c2a49b37a39ee5c4c4630"}, - {file = "regex-2024.4.28-cp311-cp311-win_amd64.whl", hash = "sha256:fc0916c4295c64d6890a46e02d4482bb5ccf33bf1a824c0eaa9e83b148291f90"}, - {file = "regex-2024.4.28-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:08a1749f04fee2811c7617fdd46d2e46d09106fa8f475c884b65c01326eb15c5"}, - {file = "regex-2024.4.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b8eb28995771c087a73338f695a08c9abfdf723d185e57b97f6175c5051ff1ae"}, - {file = "regex-2024.4.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd7ef715ccb8040954d44cfeff17e6b8e9f79c8019daae2fd30a8806ef5435c0"}, - {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb0315a2b26fde4005a7c401707c5352df274460f2f85b209cf6024271373013"}, - {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fc053228a6bd3a17a9b0a3f15c3ab3cf95727b00557e92e1cfe094b88cc662"}, - {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fe9739a686dc44733d52d6e4f7b9c77b285e49edf8570754b322bca6b85b4cc"}, - {file = "regex-2024.4.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74fcf77d979364f9b69fcf8200849ca29a374973dc193a7317698aa37d8b01c"}, - {file = "regex-2024.4.28-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:965fd0cf4694d76f6564896b422724ec7b959ef927a7cb187fc6b3f4e4f59833"}, - {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2fef0b38c34ae675fcbb1b5db760d40c3fc3612cfa186e9e50df5782cac02bcd"}, - {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bc365ce25f6c7c5ed70e4bc674f9137f52b7dd6a125037f9132a7be52b8a252f"}, - {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ac69b394764bb857429b031d29d9604842bc4cbfd964d764b1af1868eeebc4f0"}, - {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:144a1fc54765f5c5c36d6d4b073299832aa1ec6a746a6452c3ee7b46b3d3b11d"}, - {file = "regex-2024.4.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2630ca4e152c221072fd4a56d4622b5ada876f668ecd24d5ab62544ae6793ed6"}, - {file = "regex-2024.4.28-cp312-cp312-win32.whl", hash = "sha256:7f3502f03b4da52bbe8ba962621daa846f38489cae5c4a7b5d738f15f6443d17"}, - {file = "regex-2024.4.28-cp312-cp312-win_amd64.whl", hash = "sha256:0dd3f69098511e71880fb00f5815db9ed0ef62c05775395968299cb400aeab82"}, - {file = "regex-2024.4.28-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:374f690e1dd0dbdcddea4a5c9bdd97632cf656c69113f7cd6a361f2a67221cb6"}, - {file = "regex-2024.4.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f87ae6b96374db20f180eab083aafe419b194e96e4f282c40191e71980c666"}, - {file = "regex-2024.4.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5dbc1bcc7413eebe5f18196e22804a3be1bfdfc7e2afd415e12c068624d48247"}, - {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f85151ec5a232335f1be022b09fbbe459042ea1951d8a48fef251223fc67eee1"}, - {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57ba112e5530530fd175ed550373eb263db4ca98b5f00694d73b18b9a02e7185"}, - {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:224803b74aab56aa7be313f92a8d9911dcade37e5f167db62a738d0c85fdac4b"}, - {file = "regex-2024.4.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a54a047b607fd2d2d52a05e6ad294602f1e0dec2291152b745870afc47c1397"}, - {file = "regex-2024.4.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a2a512d623f1f2d01d881513af9fc6a7c46e5cfffb7dc50c38ce959f9246c94"}, - {file = "regex-2024.4.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c06bf3f38f0707592898428636cbb75d0a846651b053a1cf748763e3063a6925"}, - {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1031a5e7b048ee371ab3653aad3030ecfad6ee9ecdc85f0242c57751a05b0ac4"}, - {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7a353ebfa7154c871a35caca7bfd8f9e18666829a1dc187115b80e35a29393e"}, - {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7e76b9cfbf5ced1aca15a0e5b6f229344d9b3123439ffce552b11faab0114a02"}, - {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5ce479ecc068bc2a74cb98dd8dba99e070d1b2f4a8371a7dfe631f85db70fe6e"}, - {file = "regex-2024.4.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d77b6f63f806578c604dca209280e4c54f0fa9a8128bb8d2cc5fb6f99da4150"}, - {file = "regex-2024.4.28-cp38-cp38-win32.whl", hash = "sha256:d84308f097d7a513359757c69707ad339da799e53b7393819ec2ea36bc4beb58"}, - {file = "regex-2024.4.28-cp38-cp38-win_amd64.whl", hash = "sha256:2cc1b87bba1dd1a898e664a31012725e48af826bf3971e786c53e32e02adae6c"}, - {file = "regex-2024.4.28-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7413167c507a768eafb5424413c5b2f515c606be5bb4ef8c5dee43925aa5718b"}, - {file = "regex-2024.4.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:108e2dcf0b53a7c4ab8986842a8edcb8ab2e59919a74ff51c296772e8e74d0ae"}, - {file = "regex-2024.4.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f1c5742c31ba7d72f2dedf7968998730664b45e38827637e0f04a2ac7de2f5f1"}, - {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecc6148228c9ae25ce403eade13a0961de1cb016bdb35c6eafd8e7b87ad028b1"}, - {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7d893c8cf0e2429b823ef1a1d360a25950ed11f0e2a9df2b5198821832e1947"}, - {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4290035b169578ffbbfa50d904d26bec16a94526071ebec3dadbebf67a26b25e"}, - {file = "regex-2024.4.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a22ae1cfd82e4ffa2066eb3390777dc79468f866f0625261a93e44cdf6482b"}, - {file = "regex-2024.4.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd24fd140b69f0b0bcc9165c397e9b2e89ecbeda83303abf2a072609f60239e2"}, - {file = "regex-2024.4.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:39fb166d2196413bead229cd64a2ffd6ec78ebab83fff7d2701103cf9f4dfd26"}, - {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9301cc6db4d83d2c0719f7fcda37229691745168bf6ae849bea2e85fc769175d"}, - {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c3d389e8d76a49923683123730c33e9553063d9041658f23897f0b396b2386f"}, - {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:99ef6289b62042500d581170d06e17f5353b111a15aa6b25b05b91c6886df8fc"}, - {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b91d529b47798c016d4b4c1d06cc826ac40d196da54f0de3c519f5a297c5076a"}, - {file = "regex-2024.4.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:43548ad74ea50456e1c68d3c67fff3de64c6edb85bcd511d1136f9b5376fc9d1"}, - {file = "regex-2024.4.28-cp39-cp39-win32.whl", hash = "sha256:05d9b6578a22db7dedb4df81451f360395828b04f4513980b6bd7a1412c679cc"}, - {file = "regex-2024.4.28-cp39-cp39-win_amd64.whl", hash = "sha256:3986217ec830c2109875be740531feb8ddafe0dfa49767cdcd072ed7e8927962"}, - {file = "regex-2024.4.28.tar.gz", hash = "sha256:83ab366777ea45d58f72593adf35d36ca911ea8bd838483c1823b883a121b0e4"}, -] - -[[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -description = "OAuthlib authentication support for Requests." -optional = false -python-versions = ">=3.4" -files = [ - {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, - {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, -] - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" - -[package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -description = "A utility belt for advanced users of python-requests" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, - {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, -] - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - -[[package]] -name = "respx" -version = "0.21.1" -description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." -optional = false -python-versions = ">=3.7" -files = [ - {file = "respx-0.21.1-py2.py3-none-any.whl", hash = "sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20"}, - {file = "respx-0.21.1.tar.gz", hash = "sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af"}, -] - -[package.dependencies] -httpx = ">=0.21.0" - -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rsa" -version = "4.9" -description = "Pure-Python RSA implementation" -optional = false -python-versions = ">=3.6,<4" -files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - -[[package]] -name = "ruff" -version = "0.5.7" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, - {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, - {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, - {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, - {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, - {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, - {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, -] - -[[package]] -name = "s3transfer" -version = "0.10.1" -description = "An Amazon S3 Transfer Manager" -optional = false -python-versions = ">= 3.8" -files = [ - {file = "s3transfer-0.10.1-py3-none-any.whl", hash = "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d"}, - {file = "s3transfer-0.10.1.tar.gz", hash = "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19"}, -] - -[package.dependencies] -botocore = ">=1.33.2,<2.0a.0" - -[package.extras] -crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] - -[[package]] -name = "setuptools" -version = "78.1.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -files = [ - {file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"}, - {file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "shapely" -version = "2.0.4" -description = "Manipulation and analysis of geometric objects" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shapely-2.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:011b77153906030b795791f2fdfa2d68f1a8d7e40bce78b029782ade3afe4f2f"}, - {file = "shapely-2.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9831816a5d34d5170aa9ed32a64982c3d6f4332e7ecfe62dc97767e163cb0b17"}, - {file = "shapely-2.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c4849916f71dc44e19ed370421518c0d86cf73b26e8656192fcfcda08218fbd"}, - {file = "shapely-2.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841f93a0e31e4c64d62ea570d81c35de0f6cea224568b2430d832967536308e6"}, - {file = "shapely-2.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b4431f522b277c79c34b65da128029a9955e4481462cbf7ebec23aab61fc58"}, - {file = "shapely-2.0.4-cp310-cp310-win32.whl", hash = "sha256:92a41d936f7d6743f343be265ace93b7c57f5b231e21b9605716f5a47c2879e7"}, - {file = "shapely-2.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:30982f79f21bb0ff7d7d4a4e531e3fcaa39b778584c2ce81a147f95be1cd58c9"}, - {file = "shapely-2.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de0205cb21ad5ddaef607cda9a3191eadd1e7a62a756ea3a356369675230ac35"}, - {file = "shapely-2.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7d56ce3e2a6a556b59a288771cf9d091470116867e578bebced8bfc4147fbfd7"}, - {file = "shapely-2.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:58b0ecc505bbe49a99551eea3f2e8a9b3b24b3edd2a4de1ac0dc17bc75c9ec07"}, - {file = "shapely-2.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:790a168a808bd00ee42786b8ba883307c0e3684ebb292e0e20009588c426da47"}, - {file = "shapely-2.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4310b5494271e18580d61022c0857eb85d30510d88606fa3b8314790df7f367d"}, - {file = "shapely-2.0.4-cp311-cp311-win32.whl", hash = "sha256:63f3a80daf4f867bd80f5c97fbe03314348ac1b3b70fb1c0ad255a69e3749879"}, - {file = "shapely-2.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:c52ed79f683f721b69a10fb9e3d940a468203f5054927215586c5d49a072de8d"}, - {file = "shapely-2.0.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5bbd974193e2cc274312da16b189b38f5f128410f3377721cadb76b1e8ca5328"}, - {file = "shapely-2.0.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:41388321a73ba1a84edd90d86ecc8bfed55e6a1e51882eafb019f45895ec0f65"}, - {file = "shapely-2.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0776c92d584f72f1e584d2e43cfc5542c2f3dd19d53f70df0900fda643f4bae6"}, - {file = "shapely-2.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c75c98380b1ede1cae9a252c6dc247e6279403fae38c77060a5e6186c95073ac"}, - {file = "shapely-2.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3e700abf4a37b7b8b90532fa6ed5c38a9bfc777098bc9fbae5ec8e618ac8f30"}, - {file = "shapely-2.0.4-cp312-cp312-win32.whl", hash = "sha256:4f2ab0faf8188b9f99e6a273b24b97662194160cc8ca17cf9d1fb6f18d7fb93f"}, - {file = "shapely-2.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:03152442d311a5e85ac73b39680dd64a9892fa42bb08fd83b3bab4fe6999bfa0"}, - {file = "shapely-2.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:994c244e004bc3cfbea96257b883c90a86e8cbd76e069718eb4c6b222a56f78b"}, - {file = "shapely-2.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05ffd6491e9e8958b742b0e2e7c346635033d0a5f1a0ea083547fcc854e5d5cf"}, - {file = "shapely-2.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbdc1140a7d08faa748256438291394967aa54b40009f54e8d9825e75ef6113"}, - {file = "shapely-2.0.4-cp37-cp37m-win32.whl", hash = "sha256:5af4cd0d8cf2912bd95f33586600cac9c4b7c5053a036422b97cfe4728d2eb53"}, - {file = "shapely-2.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:464157509ce4efa5ff285c646a38b49f8c5ef8d4b340f722685b09bb033c5ccf"}, - {file = "shapely-2.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:489c19152ec1f0e5c5e525356bcbf7e532f311bff630c9b6bc2db6f04da6a8b9"}, - {file = "shapely-2.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b79bbd648664aa6f44ef018474ff958b6b296fed5c2d42db60078de3cffbc8aa"}, - {file = "shapely-2.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:674d7baf0015a6037d5758496d550fc1946f34bfc89c1bf247cabdc415d7747e"}, - {file = "shapely-2.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cd4ccecc5ea5abd06deeaab52fcdba372f649728050c6143cc405ee0c166679"}, - {file = "shapely-2.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb5cdcbbe3080181498931b52a91a21a781a35dcb859da741c0345c6402bf00c"}, - {file = "shapely-2.0.4-cp38-cp38-win32.whl", hash = "sha256:55a38dcd1cee2f298d8c2ebc60fc7d39f3b4535684a1e9e2f39a80ae88b0cea7"}, - {file = "shapely-2.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec555c9d0db12d7fd777ba3f8b75044c73e576c720a851667432fabb7057da6c"}, - {file = "shapely-2.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9103abd1678cb1b5f7e8e1af565a652e036844166c91ec031eeb25c5ca8af0"}, - {file = "shapely-2.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:263bcf0c24d7a57c80991e64ab57cba7a3906e31d2e21b455f493d4aab534aaa"}, - {file = "shapely-2.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddf4a9bfaac643e62702ed662afc36f6abed2a88a21270e891038f9a19bc08fc"}, - {file = "shapely-2.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:485246fcdb93336105c29a5cfbff8a226949db37b7473c89caa26c9bae52a242"}, - {file = "shapely-2.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8de4578e838a9409b5b134a18ee820730e507b2d21700c14b71a2b0757396acc"}, - {file = "shapely-2.0.4-cp39-cp39-win32.whl", hash = "sha256:9dab4c98acfb5fb85f5a20548b5c0abe9b163ad3525ee28822ffecb5c40e724c"}, - {file = "shapely-2.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:31c19a668b5a1eadab82ff070b5a260478ac6ddad3a5b62295095174a8d26398"}, - {file = "shapely-2.0.4.tar.gz", hash = "sha256:5dc736127fac70009b8d309a0eeb74f3e08979e530cf7017f2f507ef62e6cfb8"}, -] - -[package.dependencies] -numpy = ">=1.14,<3" - -[package.extras] -docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.29" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, - {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, - {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, - {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, - {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, - {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, - {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, - {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, - {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, -] - -[package.dependencies] -greenlet = {version = "!=0.4.17", optional = true, markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\" or extra == \"asyncio\""} -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - -[[package]] -name = "starlette" -version = "0.40.0" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -files = [ - {file = "starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4"}, - {file = "starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "sympy" -version = "1.12" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, - {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, -] - -[package.dependencies] -mpmath = ">=0.19" - -[[package]] -name = "tabulate" -version = "0.9.0" -description = "Pretty-print tabular data" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, - {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, -] - -[package.extras] -widechars = ["wcwidth"] - -[[package]] -name = "tenacity" -version = "8.2.3" -description = "Retry code until it succeeds" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, - {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, -] - -[package.extras] -doc = ["reno", "sphinx", "tornado (>=4.5)"] - -[[package]] -name = "tiktoken" -version = "0.7.0" -description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tiktoken-0.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485f3cc6aba7c6b6ce388ba634fbba656d9ee27f766216f45146beb4ac18b25f"}, - {file = "tiktoken-0.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e54be9a2cd2f6d6ffa3517b064983fb695c9a9d8aa7d574d1ef3c3f931a99225"}, - {file = "tiktoken-0.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79383a6e2c654c6040e5f8506f3750db9ddd71b550c724e673203b4f6b4b4590"}, - {file = "tiktoken-0.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d4511c52caacf3c4981d1ae2df85908bd31853f33d30b345c8b6830763f769c"}, - {file = "tiktoken-0.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13c94efacdd3de9aff824a788353aa5749c0faee1fbe3816df365ea450b82311"}, - {file = "tiktoken-0.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8e58c7eb29d2ab35a7a8929cbeea60216a4ccdf42efa8974d8e176d50c9a3df5"}, - {file = "tiktoken-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:21a20c3bd1dd3e55b91c1331bf25f4af522c525e771691adbc9a69336fa7f702"}, - {file = "tiktoken-0.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:10c7674f81e6e350fcbed7c09a65bca9356eaab27fb2dac65a1e440f2bcfe30f"}, - {file = "tiktoken-0.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:084cec29713bc9d4189a937f8a35dbdfa785bd1235a34c1124fe2323821ee93f"}, - {file = "tiktoken-0.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:811229fde1652fedcca7c6dfe76724d0908775b353556d8a71ed74d866f73f7b"}, - {file = "tiktoken-0.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b6e7dc2e7ad1b3757e8a24597415bafcfb454cebf9a33a01f2e6ba2e663992"}, - {file = "tiktoken-0.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1063c5748be36344c7e18c7913c53e2cca116764c2080177e57d62c7ad4576d1"}, - {file = "tiktoken-0.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20295d21419bfcca092644f7e2f2138ff947a6eb8cfc732c09cc7d76988d4a89"}, - {file = "tiktoken-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:959d993749b083acc57a317cbc643fb85c014d055b2119b739487288f4e5d1cb"}, - {file = "tiktoken-0.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:71c55d066388c55a9c00f61d2c456a6086673ab7dec22dd739c23f77195b1908"}, - {file = "tiktoken-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09ed925bccaa8043e34c519fbb2f99110bd07c6fd67714793c21ac298e449410"}, - {file = "tiktoken-0.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03c6c40ff1db0f48a7b4d2dafeae73a5607aacb472fa11f125e7baf9dce73704"}, - {file = "tiktoken-0.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d20b5c6af30e621b4aca094ee61777a44118f52d886dbe4f02b70dfe05c15350"}, - {file = "tiktoken-0.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d427614c3e074004efa2f2411e16c826f9df427d3c70a54725cae860f09e4bf4"}, - {file = "tiktoken-0.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c46d7af7b8c6987fac9b9f61041b452afe92eb087d29c9ce54951280f899a97"}, - {file = "tiktoken-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0bc603c30b9e371e7c4c7935aba02af5994a909fc3c0fe66e7004070858d3f8f"}, - {file = "tiktoken-0.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2398fecd38c921bcd68418675a6d155fad5f5e14c2e92fcf5fe566fa5485a858"}, - {file = "tiktoken-0.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f5f6afb52fb8a7ea1c811e435e4188f2bef81b5e0f7a8635cc79b0eef0193d6"}, - {file = "tiktoken-0.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:861f9ee616766d736be4147abac500732b505bf7013cfaf019b85892637f235e"}, - {file = "tiktoken-0.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54031f95c6939f6b78122c0aa03a93273a96365103793a22e1793ee86da31685"}, - {file = "tiktoken-0.7.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fffdcb319b614cf14f04d02a52e26b1d1ae14a570f90e9b55461a72672f7b13d"}, - {file = "tiktoken-0.7.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c72baaeaefa03ff9ba9688624143c858d1f6b755bb85d456d59e529e17234769"}, - {file = "tiktoken-0.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:131b8aeb043a8f112aad9f46011dced25d62629091e51d9dc1adbf4a1cc6aa98"}, - {file = "tiktoken-0.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cabc6dc77460df44ec5b879e68692c63551ae4fae7460dd4ff17181df75f1db7"}, - {file = "tiktoken-0.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8d57f29171255f74c0aeacd0651e29aa47dff6f070cb9f35ebc14c82278f3b25"}, - {file = "tiktoken-0.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ee92776fdbb3efa02a83f968c19d4997a55c8e9ce7be821ceee04a1d1ee149c"}, - {file = "tiktoken-0.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e215292e99cb41fbc96988ef62ea63bb0ce1e15f2c147a61acc319f8b4cbe5bf"}, - {file = "tiktoken-0.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a81bac94769cab437dd3ab0b8a4bc4e0f9cf6835bcaa88de71f39af1791727a"}, - {file = "tiktoken-0.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d6d73ea93e91d5ca771256dfc9d1d29f5a554b83821a1dc0891987636e0ae226"}, - {file = "tiktoken-0.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:2bcb28ddf79ffa424f171dfeef9a4daff61a94c631ca6813f43967cb263b83b9"}, - {file = "tiktoken-0.7.0.tar.gz", hash = "sha256:1077266e949c24e0291f6c350433c6f0971365ece2b173a23bc3b9f9defef6b6"}, -] - -[package.dependencies] -regex = ">=2022.1.18" -requests = ">=2.26.0" - -[package.extras] -blobfile = ["blobfile (>=2)"] - -[[package]] -name = "tokenizers" -version = "0.19.1" -description = "" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tokenizers-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:952078130b3d101e05ecfc7fc3640282d74ed26bcf691400f872563fca15ac97"}, - {file = "tokenizers-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82c8b8063de6c0468f08e82c4e198763e7b97aabfe573fd4cf7b33930ca4df77"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f03727225feaf340ceeb7e00604825addef622d551cbd46b7b775ac834c1e1c4"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:453e4422efdfc9c6b6bf2eae00d5e323f263fff62b29a8c9cd526c5003f3f642"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02e81bf089ebf0e7f4df34fa0207519f07e66d8491d963618252f2e0729e0b46"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b07c538ba956843833fee1190cf769c60dc62e1cf934ed50d77d5502194d63b1"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28cab1582e0eec38b1f38c1c1fb2e56bce5dc180acb1724574fc5f47da2a4fe"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b01afb7193d47439f091cd8f070a1ced347ad0f9144952a30a41836902fe09e"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7fb297edec6c6841ab2e4e8f357209519188e4a59b557ea4fafcf4691d1b4c98"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e8a3dd055e515df7054378dc9d6fa8c8c34e1f32777fb9a01fea81496b3f9d3"}, - {file = "tokenizers-0.19.1-cp310-none-win32.whl", hash = "sha256:7ff898780a155ea053f5d934925f3902be2ed1f4d916461e1a93019cc7250837"}, - {file = "tokenizers-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:bea6f9947e9419c2fda21ae6c32871e3d398cba549b93f4a65a2d369662d9403"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c88d1481f1882c2e53e6bb06491e474e420d9ac7bdff172610c4f9ad3898059"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddf672ed719b4ed82b51499100f5417d7d9f6fb05a65e232249268f35de5ed14"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dadc509cc8a9fe460bd274c0e16ac4184d0958117cf026e0ea8b32b438171594"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfedf31824ca4915b511b03441784ff640378191918264268e6923da48104acc"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac11016d0a04aa6487b1513a3a36e7bee7eec0e5d30057c9c0408067345c48d2"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76951121890fea8330d3a0df9a954b3f2a37e3ec20e5b0530e9a0044ca2e11fe"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b342d2ce8fc8d00f376af068e3274e2e8649562e3bc6ae4a67784ded6b99428d"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16ff18907f4909dca9b076b9c2d899114dd6abceeb074eca0c93e2353f943aa"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:706a37cc5332f85f26efbe2bdc9ef8a9b372b77e4645331a405073e4b3a8c1c6"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16baac68651701364b0289979ecec728546133e8e8fe38f66fe48ad07996b88b"}, - {file = "tokenizers-0.19.1-cp311-none-win32.whl", hash = "sha256:9ed240c56b4403e22b9584ee37d87b8bfa14865134e3e1c3fb4b2c42fafd3256"}, - {file = "tokenizers-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:ad57d59341710b94a7d9dbea13f5c1e7d76fd8d9bcd944a7a6ab0b0da6e0cc66"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:621d670e1b1c281a1c9698ed89451395d318802ff88d1fc1accff0867a06f153"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d924204a3dbe50b75630bd16f821ebda6a5f729928df30f582fb5aade90c818a"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f3fefdc0446b1a1e6d81cd4c07088ac015665d2e812f6dbba4a06267d1a2c95"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9620b78e0b2d52ef07b0d428323fb34e8ea1219c5eac98c2596311f20f1f9266"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04ce49e82d100594715ac1b2ce87d1a36e61891a91de774755f743babcd0dd52"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5c2ff13d157afe413bf7e25789879dd463e5a4abfb529a2d8f8473d8042e28f"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3174c76efd9d08f836bfccaca7cfec3f4d1c0a4cf3acbc7236ad577cc423c840"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9d5b6c0e7a1e979bec10ff960fae925e947aab95619a6fdb4c1d8ff3708ce3"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a179856d1caee06577220ebcfa332af046d576fb73454b8f4d4b0ba8324423ea"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:952b80dac1a6492170f8c2429bd11fcaa14377e097d12a1dbe0ef2fb2241e16c"}, - {file = "tokenizers-0.19.1-cp312-none-win32.whl", hash = "sha256:01d62812454c188306755c94755465505836fd616f75067abcae529c35edeb57"}, - {file = "tokenizers-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:b70bfbe3a82d3e3fb2a5e9b22a39f8d1740c96c68b6ace0086b39074f08ab89a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:bb9dfe7dae85bc6119d705a76dc068c062b8b575abe3595e3c6276480e67e3f1"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:1f0360cbea28ea99944ac089c00de7b2e3e1c58f479fb8613b6d8d511ce98267"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:71e3ec71f0e78780851fef28c2a9babe20270404c921b756d7c532d280349214"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b82931fa619dbad979c0ee8e54dd5278acc418209cc897e42fac041f5366d626"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ff5b90eabdcdaa19af697885f70fe0b714ce16709cf43d4952f1f85299e73a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e742d76ad84acbdb1a8e4694f915fe59ff6edc381c97d6dfdd054954e3478ad4"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8c5d59d7b59885eab559d5bc082b2985555a54cda04dda4c65528d90ad252ad"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2da5c32ed869bebd990c9420df49813709e953674c0722ff471a116d97b22d"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:638e43936cc8b2cbb9f9d8dde0fe5e7e30766a3318d2342999ae27f68fdc9bd6"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:78e769eb3b2c79687d9cb0f89ef77223e8e279b75c0a968e637ca7043a84463f"}, - {file = "tokenizers-0.19.1-cp37-none-win32.whl", hash = "sha256:72791f9bb1ca78e3ae525d4782e85272c63faaef9940d92142aa3eb79f3407a3"}, - {file = "tokenizers-0.19.1-cp37-none-win_amd64.whl", hash = "sha256:f3bbb7a0c5fcb692950b041ae11067ac54826204318922da754f908d95619fbc"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:07f9295349bbbcedae8cefdbcfa7f686aa420be8aca5d4f7d1ae6016c128c0c5"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10a707cc6c4b6b183ec5dbfc5c34f3064e18cf62b4a938cb41699e33a99e03c1"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6309271f57b397aa0aff0cbbe632ca9d70430839ca3178bf0f06f825924eca22"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ad23d37d68cf00d54af184586d79b84075ada495e7c5c0f601f051b162112dc"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:427c4f0f3df9109314d4f75b8d1f65d9477033e67ffaec4bca53293d3aca286d"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e83a31c9cf181a0a3ef0abad2b5f6b43399faf5da7e696196ddd110d332519ee"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c27b99889bd58b7e301468c0838c5ed75e60c66df0d4db80c08f43462f82e0d3"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bac0b0eb952412b0b196ca7a40e7dce4ed6f6926489313414010f2e6b9ec2adf"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8a6298bde623725ca31c9035a04bf2ef63208d266acd2bed8c2cb7d2b7d53ce6"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:08a44864e42fa6d7d76d7be4bec62c9982f6f6248b4aa42f7302aa01e0abfd26"}, - {file = "tokenizers-0.19.1-cp38-none-win32.whl", hash = "sha256:1de5bc8652252d9357a666e609cb1453d4f8e160eb1fb2830ee369dd658e8975"}, - {file = "tokenizers-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:0bcce02bf1ad9882345b34d5bd25ed4949a480cf0e656bbd468f4d8986f7a3f1"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0b9394bd204842a2a1fd37fe29935353742be4a3460b6ccbaefa93f58a8df43d"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4692ab92f91b87769d950ca14dbb61f8a9ef36a62f94bad6c82cc84a51f76f6a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6258c2ef6f06259f70a682491c78561d492e885adeaf9f64f5389f78aa49a051"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85cf76561fbd01e0d9ea2d1cbe711a65400092bc52b5242b16cfd22e51f0c58"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:670b802d4d82bbbb832ddb0d41df7015b3e549714c0e77f9bed3e74d42400fbe"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85aa3ab4b03d5e99fdd31660872249df5e855334b6c333e0bc13032ff4469c4a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf001afbbed111a79ca47d75941e9e5361297a87d186cbfc11ed45e30b5daba"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c89aa46c269e4e70c4d4f9d6bc644fcc39bb409cb2a81227923404dd6f5227"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39c1ec76ea1027438fafe16ecb0fb84795e62e9d643444c1090179e63808c69d"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c2a0d47a89b48d7daa241e004e71fb5a50533718897a4cd6235cb846d511a478"}, - {file = "tokenizers-0.19.1-cp39-none-win32.whl", hash = "sha256:61b7fe8886f2e104d4caf9218b157b106207e0f2a4905c9c7ac98890688aabeb"}, - {file = "tokenizers-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:f97660f6c43efd3e0bfd3f2e3e5615bf215680bad6ee3d469df6454b8c6e8256"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b11853f17b54c2fe47742c56d8a33bf49ce31caf531e87ac0d7d13d327c9334"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d26194ef6c13302f446d39972aaa36a1dda6450bc8949f5eb4c27f51191375bd"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e8d1ed93beda54bbd6131a2cb363a576eac746d5c26ba5b7556bc6f964425594"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca407133536f19bdec44b3da117ef0d12e43f6d4b56ac4c765f37eca501c7bda"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce05fde79d2bc2e46ac08aacbc142bead21614d937aac950be88dc79f9db9022"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:35583cd46d16f07c054efd18b5d46af4a2f070a2dd0a47914e66f3ff5efb2b1e"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:43350270bfc16b06ad3f6f07eab21f089adb835544417afda0f83256a8bf8b75"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b4399b59d1af5645bcee2072a463318114c39b8547437a7c2d6a186a1b5a0e2d"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6852c5b2a853b8b0ddc5993cd4f33bfffdca4fcc5d52f89dd4b8eada99379285"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd266ae85c3d39df2f7e7d0e07f6c41a55e9a3123bb11f854412952deacd828"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecb2651956eea2aa0a2d099434134b1b68f1c31f9a5084d6d53f08ed43d45ff2"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b279ab506ec4445166ac476fb4d3cc383accde1ea152998509a94d82547c8e2a"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89183e55fb86e61d848ff83753f64cded119f5d6e1f553d14ffee3700d0a4a49"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2edbc75744235eea94d595a8b70fe279dd42f3296f76d5a86dde1d46e35f574"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e64bfde9a723274e9a71630c3e9494ed7b4c0f76a1faacf7fe294cd26f7ae7c"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b5ca92bfa717759c052e345770792d02d1f43b06f9e790ca0a1db62838816f3"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f8a20266e695ec9d7a946a019c1d5ca4eddb6613d4f466888eee04f16eedb85"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63c38f45d8f2a2ec0f3a20073cccb335b9f99f73b3c69483cd52ebc75369d8a1"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dd26e3afe8a7b61422df3176e06664503d3f5973b94f45d5c45987e1cb711876"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:eddd5783a4a6309ce23432353cdb36220e25cbb779bfa9122320666508b44b88"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:56ae39d4036b753994476a1b935584071093b55c7a72e3b8288e68c313ca26e7"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9939ca7e58c2758c01b40324a59c034ce0cebad18e0d4563a9b1beab3018243"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c330c0eb815d212893c67a032e9dc1b38a803eccb32f3e8172c19cc69fbb439"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec11802450a2487cdf0e634b750a04cbdc1c4d066b97d94ce7dd2cb51ebb325b"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b718f316b596f36e1dae097a7d5b91fc5b85e90bf08b01ff139bd8953b25af"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ed69af290c2b65169f0ba9034d1dc39a5db9459b32f1dd8b5f3f32a3fcf06eab"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f8a9c828277133af13f3859d1b6bf1c3cb6e9e1637df0e45312e6b7c2e622b1f"}, - {file = "tokenizers-0.19.1.tar.gz", hash = "sha256:ee59e6680ed0fdbe6b724cf38bd70400a0c1dd623b07ac729087270caeac88e3"}, -] - -[package.dependencies] -huggingface-hub = ">=0.16.4,<1.0" - -[package.extras] -dev = ["tokenizers[testing]"] -docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] -testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tqdm" -version = "4.66.3" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53"}, - {file = "tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "typer" -version = "0.12.3" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -files = [ - {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, - {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - -[[package]] -name = "types-requests" -version = "2.31.0.6" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.7" -files = [ - {file = "types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"}, - {file = "types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9"}, -] - -[package.dependencies] -types-urllib3 = "*" - -[[package]] -name = "types-urllib3" -version = "1.26.25.14" -description = "Typing stubs for urllib3" -optional = false -python-versions = "*" -files = [ - {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, - {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "typing-inspect" -version = "0.9.0" -description = "Runtime inspection utilities for typing module." -optional = false -python-versions = "*" -files = [ - {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, - {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, -] - -[package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" - -[[package]] -name = "tzdata" -version = "2024.1" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - -[[package]] -name = "urllib3" -version = "1.26.20" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, - {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, -] - -[package.extras] -brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "uvicorn" -version = "0.29.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.8" -files = [ - {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, - {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} -h11 = ">=0.8" -httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} -python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} - -[package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "uvloop" -version = "0.19.0" -description = "Fast implementation of asyncio event loop on top of libuv" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, - {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, -] - -[package.extras] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] - -[[package]] -name = "virtualenv" -version = "20.26.6" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, - {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "watchfiles" -version = "0.21.0" -description = "Simple, modern and high performance file watching and code reload in python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, - {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, - {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, - {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, - {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, - {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, - {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, - {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, - {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, - {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, - {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, - {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, - {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, - {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, - {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, -] - -[package.dependencies] -anyio = ">=3.0.0" - -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "websockets" -version = "12.0" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, - {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, - {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, - {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, - {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, - {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, - {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, - {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, - {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, - {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, - {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, - {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, - {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, - {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, - {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, - {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, - {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, - {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, - {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, - {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, - {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, - {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, - {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, - {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, - {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, -] - -[[package]] -name = "werkzeug" -version = "3.0.6" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.8" -files = [ - {file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"}, - {file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - -[[package]] -name = "wrapt" -version = "1.16.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.6" -files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, -] - -[[package]] -name = "yarl" -version = "1.18.3" -description = "Yet another URL library" -optional = false -python-versions = ">=3.9" -files = [ - {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, - {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, - {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, - {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, - {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, - {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, - {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, - {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, - {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, - {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, - {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, - {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, - {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, - {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, - {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.0" - -[[package]] -name = "zipp" -version = "3.19.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, - {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - -[extras] -langchain = ["langchain"] -llama-index = [] -openai = ["openai"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.9,<4.0" -content-hash = "ac31e0db93cfcbf0adbb201a20b1547d1af5347437062d55142d3b24838f711e" diff --git a/pyproject.toml b/pyproject.toml index a7c7958ac..ceb7ea368 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,77 +1,64 @@ -[tool.poetry] +[project] name = "langfuse" - -version = "3.2.1" +version = "4.8.0b1" description = "A client library for accessing langfuse" -authors = ["langfuse "] -license = "MIT" readme = "README.md" +authors = [{ name = "langfuse", email = "developers@langfuse.com" }] +license = "MIT" +license-files = ["LICENSE"] +requires-python = ">=3.10,<4.0" +dependencies = [ + "httpx>=0.15.4,<1.0", + "pydantic>=2,<3", + "backoff>=1.10.0", + "wrapt>=1.14,<2", + "packaging>=23.2,<27.0", + "opentelemetry-api>=1.33.1,<2", + "opentelemetry-sdk>=1.33.1,<2", + "opentelemetry-exporter-otlp-proto-http>=1.33.1,<2", +] -[tool.poetry.dependencies] -python = ">=3.9,<4.0" -httpx = ">=0.15.4,<1.0" -pydantic = ">=1.10.7, <3.0" -backoff = ">=1.10.0" -openai = { version = ">=0.27.8", optional = true } -wrapt = "^1.14" -langchain = { version = ">=0.0.309", optional = true } -packaging = ">=23.2,<26.0" -requests = "^2" -opentelemetry-api = "^1.33.1" -opentelemetry-sdk = "^1.33.1" -opentelemetry-exporter-otlp = "^1.33.1" +[dependency-groups] +dev = [ + "pytest>=7.4,<9.0", + "pytest-timeout>=2.1.0,<3", + "pytest-xdist>=3.3.1,<4", + "pre-commit>=3.2.2,<4", + "pytest-asyncio>=0.21.1,<1.2.0", + "pytest-httpserver>=1.0.8,<2", + "ruff>=0.15.2,<0.16", + "mypy>=1.0.0,<2", + "openai>=0.27.8", + "langchain-openai>=0.0.5,<0.4", + "langchain>=1,<2", + "langgraph>=1,<2", + "autoevals>=0.0.130,<0.1", + "opentelemetry-instrumentation-threading>=0.59b0,<1", + "tenacity>=9.1.4", +] +docs = [ + "pdoc>=15.0.4,<16", +] -[tool.poetry.group.dev.dependencies] -pytest = ">=7.4,<9.0" -chromadb = ">=0.4.2,<0.6.0" -tiktoken = "0.7.0" -pytest-timeout = "^2.1.0" -pytest-xdist = "^3.3.1" -respx = ">=0.20.2,<0.22.0" -google-search-results = "^2.4.2" -huggingface_hub = ">=0.16.4,<0.25.0" -pre-commit = "^3.2.2" -anthropic = ">=0.17.0,<1" -bs4 = ">=0.0.1,<0.0.3" -lark = "^1.1.7" -pytest-asyncio = ">=0.21.1,<0.24.0" -pytest-httpserver = "^1.0.8" -boto3 = "^1.28.59" -ruff = ">=0.1.8,<0.6.0" -mypy = "^1.0.0" -langchain-mistralai = ">=0.0.1,<0.3" -google-cloud-aiplatform = "^1.38.1" -cohere = ">=4.46,<6.0" -langchain-google-vertexai = ">=1.0.0,<3.0.0" -langchain-openai = ">=0.0.5,<0.3" -dashscope = "^1.14.1" -pymongo = "^4.6.1" -llama-index-llms-anthropic = ">=0.1.1,<0.6" -bson = "^0.5.10" -langchain-anthropic = ">=0.1.4,<0.4" -langchain-groq = ">=0.1.3,<0.3" -langchain-aws = ">=0.1.3,<0.3" -langchain-ollama = "^0.2.0" -langchain-cohere = "^0.3.3" -langchain-community = ">=0.2.14,<0.4" -langgraph = "^0.2.62" +[build-system] +requires = ["uv_build>=0.11.2,<0.12.0"] +build-backend = "uv_build" -[tool.poetry.group.docs.dependencies] -pdoc = "^14.4.0" +[tool.uv] +# Basic protection against supply chain attacks +exclude-newer = "7 days" -[tool.poetry.extras] -openai = ["openai"] -langchain = ["langchain"] -llama-index = ["llama-index"] - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.uv.build-backend] +module-root = "" [tool.pytest.ini_options] log_cli = true - -[tool.poetry_bumpversion.file."langfuse/version.py"] +markers = [ + "unit: deterministic tests that run without a Langfuse server", + "e2e: tests that require a real Langfuse server or persisted backend behaviour", + "serial_e2e: e2e tests that must not share server concurrency with the rest of the suite", + "live_provider: tests that call live model providers and run as a dedicated CI suite", +] [tool.mypy] python_version = "3.12" @@ -122,12 +109,23 @@ module = [ ignore_missing_imports = true [[tool.mypy.overrides]] -module = [ - "langfuse.api.resources.*", - "langfuse.api.core.*", - "langfuse.api.client" +module = "langfuse.api.*" +disable_error_code = ["redundant-cast", "no-untyped-def"] + +[tool.ruff] +target-version = "py310" + +[tool.ruff.lint] +extend-select = [ + "I", # Import formatting + # These used to be enabled in the "local ruff.toml", + # which was never enforced; hence, enabling these will + # cause a large number of linting errors. + + # "D", # Docstring lints + # "D401", # Enforce imperative mood in docstrings ] -ignore_errors = true +exclude = ["langfuse/api/**/*.py"] -[tool.poetry.scripts] -release = "scripts.release:main" +[tool.ruff.lint.pydocstyle] +convention = "google" diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 42843016d..000000000 --- a/ruff.toml +++ /dev/null @@ -1,17 +0,0 @@ -# This is the Ruff config used locally. -# In CI, ci.ruff.toml is used instead. - -target-version = 'py38' - -[lint] -select = [ - # Enforce the use of Google-style docstrings. - "D", - # Augment the convention by requiring an imperative mood for all docstrings. - "D401", -] -exclude = ["langfuse/api/**/*.py"] - -[lint.pydocstyle] -convention = "google" - diff --git a/scripts/codex/maintenance.sh b/scripts/codex/maintenance.sh new file mode 100755 index 000000000..aba80e6a3 --- /dev/null +++ b/scripts/codex/maintenance.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" + +uv sync --locked +uv cache prune --ci >/dev/null 2>&1 || true diff --git a/scripts/codex/quick-check.sh b/scripts/codex/quick-check.sh new file mode 100755 index 000000000..fd3c1823c --- /dev/null +++ b/scripts/codex/quick-check.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" + +uv run --frozen ruff check . +uv run --frozen mypy langfuse --no-error-summary +uv run --frozen pytest -n auto --dist worksteal tests/unit diff --git a/scripts/codex/setup.sh b/scripts/codex/setup.sh new file mode 100755 index 000000000..b6e7d30f5 --- /dev/null +++ b/scripts/codex/setup.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" + +if ! command -v uv >/dev/null 2>&1; then + python3 -m pip install --user "uv==0.11.2" + export PATH="$HOME/.local/bin:$PATH" +fi + +uv sync --locked +uv run --frozen python --version diff --git a/scripts/release.py b/scripts/release.py deleted file mode 100644 index 605d02dc9..000000000 --- a/scripts/release.py +++ /dev/null @@ -1,184 +0,0 @@ -"""@private""" - -import subprocess -import sys -import argparse -import logging -import re - -# Configure logging -logging.basicConfig(level=logging.INFO, format="🤖 Release SDK - %(message)s") - - -def run_command(command, check=True): - logging.info(f"Running command: {command}") - result = subprocess.run( - command, shell=True, check=check, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - return result.stdout.decode("utf-8").strip() - - -def check_git_status(): - # Check if there are uncommitted changes - logging.info("Checking for uncommitted changes...") - status_output = run_command("git status --porcelain", check=False) - if status_output: - logging.error( - "Your working directory has uncommitted changes. Please commit or stash them before proceeding." - ) - sys.exit(1) - - # Check if the current branch is 'main' - logging.info("Checking the current branch...") - current_branch = run_command("git rev-parse --abbrev-ref HEAD") - if current_branch != "main": - logging.error( - "You are not on the 'main' branch. Please switch to 'main' before proceeding." - ) - sys.exit(1) - - # Pull the latest changes from remote 'main' - logging.info("Pulling the latest changes from remote 'main'...") - run_command("git pull origin main") - - -def get_latest_tag(): - try: - latest_tag = run_command("git describe --tags --abbrev=0") - if latest_tag.startswith("v"): - latest_tag = latest_tag[1:] - except subprocess.CalledProcessError: - latest_tag = "0.0.0" # default if no tags exist - return latest_tag - - -def increment_version(current_version, increment_type): - major, minor, patch = map(int, current_version.split(".")) - if increment_type == "patch": - patch += 1 - elif increment_type == "minor": - minor += 1 - patch = 0 - elif increment_type == "major": - major += 1 - minor = 0 - patch = 0 - return f"{major}.{minor}.{patch}" - - -def update_version_file(version): - version_file_path = "langfuse/version.py" - logging.info(f"Updating version in {version_file_path} to {version}...") - - with open(version_file_path, "r") as file: - content = file.read() - - new_content = re.sub( - r'__version__ = "\d+\.\d+\.\d+"', f'__version__ = "{version}"', content - ) - - with open(version_file_path, "w") as file: - file.write(new_content) - - logging.info(f"Updated version in {version_file_path}.") - - -def main(): - parser = argparse.ArgumentParser( - description="Automate the release process for the Langfuse Python SDK using Poetry." - ) - parser.add_argument( - "increment_type", - choices=["patch", "minor", "major"], - help="Specify the type of version increment.", - ) - args = parser.parse_args() - - increment_type = args.increment_type - - try: - logging.info("Starting release process...") - - # Preliminary checks - logging.info("Performing preliminary checks...") - check_git_status() - logging.info("Git status is clean, on 'main' branch, and up to date.") - - # Get the latest tag - current_version = get_latest_tag() - logging.info(f"Current version: v{current_version}") - - # Determine the new version - new_version = increment_version(current_version, increment_type) - logging.info(f"Proposed new version: v{new_version}") - - # Ask for user confirmation - confirm = input( - f"Do you want to proceed with the release version v{new_version}? (y/n): " - ) - if confirm.lower() != "y": - logging.info("Release process aborted by user.") - sys.exit(0) - - # Step 1: Update the version - logging.info("Step 1: Updating the version...") - run_command(f"poetry version {new_version}") - logging.info(f"Updated to version v{new_version}") - - # Update the version in langfuse/version.py - update_version_file(new_version) - - # Ask for user confirmation - confirm = input( - f"Please check the changed files in the working tree. Proceed with releasing v{new_version}? (y/n): " - ) - if confirm.lower() != "y": - logging.info("Release process aborted by user.") - sys.exit(0) - - # Step 2: Install dependencies - logging.info("Step 2: Installing dependencies...") - run_command("poetry install") - - # Step 3: Build the package - logging.info("Step 3: Building the package...") - run_command("poetry build") - - # Step 4: Commit the changes - logging.info("Step 4: Committing the changes...") - run_command(f'git commit -am "chore: release v{new_version}"') - - # Step 5: Push the commit - logging.info("Step 5: Pushing the commit...") - run_command("git push") - - # Step 6: Tag the version - logging.info("Step 6: Tagging the version...") - run_command(f"git tag v{new_version}") - - # Step 7: Push the tags - logging.info("Step 7: Pushing the tags...") - run_command("git push --tags") - - # # Step 8: Publish to PyPi - logging.info("Step 8: Publishing to PyPi...") - run_command("poetry publish") - logging.info("Published to PyPi successfully.") - - # Step 9: Prompt the user to create a GitHub release - logging.info( - "Step 9: Please create a new release on GitHub by visiting the following URL:" - ) - print( - "Go to: https://github.com/langfuse/langfuse-python/releases to create the release." - ) - logging.info("Release process completed successfully.") - - except subprocess.CalledProcessError as e: - logging.error(f"An error occurred while running command: {e.cmd}") - logging.error(e.stderr.decode("utf-8")) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/scripts/select_e2e_shard.py b/scripts/select_e2e_shard.py new file mode 100644 index 000000000..688d8468d --- /dev/null +++ b/scripts/select_e2e_shard.py @@ -0,0 +1,114 @@ +import argparse +import ast +import json +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +E2E_ROOT = REPO_ROOT / "tests" / "e2e" + +# These weights keep the existing balance close to the observed runtime split, +# while new files automatically fall back to their local test count. +HISTORICAL_WEIGHTS = { + "tests/e2e/test_batch_evaluation.py": 41, + "tests/e2e/test_core_sdk.py": 53, + "tests/e2e/test_datasets.py": 7, + "tests/e2e/test_decorators.py": 32, + "tests/e2e/test_experiments.py": 17, + "tests/e2e/test_media.py": 1, + "tests/e2e/test_prompt.py": 27, +} + + +def relative_test_path(path: Path) -> str: + return path.relative_to(REPO_ROOT).as_posix() + + +def discover_e2e_files() -> list[Path]: + return sorted(E2E_ROOT.glob("test_*.py")) + + +def count_test_functions(path: Path) -> int: + module = ast.parse(path.read_text(encoding="utf-8")) + return sum( + 1 + for node in module.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + and node.name.startswith("test_") + ) + + +def estimate_weight(path: Path) -> int: + try: + relative_path = relative_test_path(path) + except ValueError: + relative_path = None + if relative_path is not None and relative_path in HISTORICAL_WEIGHTS: + return HISTORICAL_WEIGHTS[relative_path] + + return max(count_test_functions(path), 1) + + +def assign_shards( + paths: list[Path], shard_count: int +) -> tuple[list[list[str]], list[int]]: + shard_loads = [0] * shard_count + shards: list[list[str]] = [[] for _ in range(shard_count)] + + weighted_paths = sorted( + ((estimate_weight(path), relative_test_path(path)) for path in paths), + key=lambda item: (-item[0], item[1]), + ) + + for weight, relative_path in weighted_paths: + shard_index = min( + range(shard_count), key=lambda index: (shard_loads[index], index) + ) + shards[shard_index].append(relative_path) + shard_loads[shard_index] += weight + + return [sorted(shard) for shard in shards], shard_loads + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Select the files for one e2e CI shard." + ) + parser.add_argument("--shard-index", required=True, type=int) + parser.add_argument("--shard-count", default=2, type=int) + parser.add_argument("--json", action="store_true") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + + if args.shard_count < 1: + raise SystemExit("--shard-count must be at least 1") + + if args.shard_index < 0 or args.shard_index >= args.shard_count: + raise SystemExit("--shard-index must be within the configured shard count") + + shards, shard_loads = assign_shards(discover_e2e_files(), args.shard_count) + selected_files = shards[args.shard_index] + + if args.json: + print( + json.dumps( + { + "shard_count": args.shard_count, + "shard_index": args.shard_index, + "selected_files": selected_files, + "shard_loads": shard_loads, + } + ) + ) + return 0 + + for path in selected_files: + print(path) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/api_wrapper.py b/tests/api_wrapper.py deleted file mode 100644 index 42f941550..000000000 --- a/tests/api_wrapper.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -from time import sleep - -import httpx - - -class LangfuseAPI: - def __init__(self, username=None, password=None, base_url=None): - username = username if username else os.environ["LANGFUSE_PUBLIC_KEY"] - password = password if password else os.environ["LANGFUSE_SECRET_KEY"] - self.auth = (username, password) - self.BASE_URL = base_url if base_url else os.environ["LANGFUSE_HOST"] - - def get_observation(self, observation_id): - sleep(1) - url = f"{self.BASE_URL}/api/public/observations/{observation_id}" - response = httpx.get(url, auth=self.auth) - return response.json() - - def get_scores(self, page=None, limit=None, user_id=None, name=None): - sleep(1) - params = {"page": page, "limit": limit, "userId": user_id, "name": name} - url = f"{self.BASE_URL}/api/public/scores" - response = httpx.get(url, params=params, auth=self.auth) - return response.json() - - def get_traces(self, page=None, limit=None, user_id=None, name=None): - sleep(1) - params = {"page": page, "limit": limit, "userId": user_id, "name": name} - url = f"{self.BASE_URL}/api/public/traces" - response = httpx.get(url, params=params, auth=self.auth) - return response.json() - - def get_trace(self, trace_id): - sleep(1) - url = f"{self.BASE_URL}/api/public/traces/{trace_id}" - response = httpx.get(url, auth=self.auth) - return response.json() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..0163842c6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,171 @@ +import json +from pathlib import Path +from typing import Any, Iterable, Sequence + +import pytest +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +from langfuse._client.client import Langfuse +from langfuse._client.resource_manager import LangfuseResourceManager + +SERIAL_E2E_NODEIDS = { + "tests/e2e/test_core_sdk.py::test_create_trace", + "tests/e2e/test_core_sdk.py::test_create_boolean_score", + "tests/e2e/test_core_sdk.py::test_create_categorical_score", + "tests/e2e/test_core_sdk.py::test_create_score_with_custom_timestamp", + "tests/e2e/test_decorators.py::test_return_dict_for_output", + "tests/e2e/test_decorators.py::test_media", + "tests/e2e/test_decorators.py::test_merge_metadata_and_tags", + "tests/e2e/test_experiments.py::test_boolean_score_types", + "tests/e2e/test_media.py::test_replace_media_reference_string_in_object", +} + + +class InMemorySpanExporter(SpanExporter): + """Simple in-memory exporter to collect spans for deterministic tests.""" + + def __init__(self) -> None: + self._finished_spans: list[ReadableSpan] = [] + self._stopped = False + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + if self._stopped: + return SpanExportResult.FAILURE + + self._finished_spans.extend(spans) + return SpanExportResult.SUCCESS + + def shutdown(self) -> None: + self._stopped = True + + def get_finished_spans(self) -> list[ReadableSpan]: + return list(self._finished_spans) + + def clear(self) -> None: + self._finished_spans.clear() + + +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: + for item in items: + test_group = Path(str(item.fspath)).parent.name + + if test_group == "unit": + item.add_marker(pytest.mark.unit) + continue + + if test_group == "e2e": + item.add_marker(pytest.mark.e2e) + if item.nodeid in SERIAL_E2E_NODEIDS: + item.add_marker(pytest.mark.serial_e2e) + continue + + if test_group == "live_provider": + item.add_marker(pytest.mark.e2e) + item.add_marker(pytest.mark.live_provider) + + +@pytest.fixture(autouse=True) +def reset_langfuse_state(request: pytest.FixtureRequest) -> Iterable[None]: + if request.node.get_closest_marker("unit") is None: + yield + return + + LangfuseResourceManager.reset() + yield + LangfuseResourceManager.reset() + + +@pytest.fixture +def memory_exporter() -> Iterable[InMemorySpanExporter]: + exporter = InMemorySpanExporter() + yield exporter + exporter.shutdown() + + +@pytest.fixture +def langfuse_memory_client( + monkeypatch: pytest.MonkeyPatch, memory_exporter: InMemorySpanExporter +) -> Iterable[Langfuse]: + monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "test-public-key") + monkeypatch.setenv("LANGFUSE_SECRET_KEY", "test-secret-key") + monkeypatch.setenv("LANGFUSE_BASE_URL", "http://test-host") + + tracer_provider = TracerProvider(resource=Resource.create({"service.name": "test"})) + + def mock_init(self: Any, **kwargs: Any) -> None: + import threading + + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + from langfuse._client.span_filter import is_default_export_span + + self.public_key = kwargs.get("public_key", "test-public-key") + blocked_scopes = kwargs.get("blocked_instrumentation_scopes") + self.blocked_instrumentation_scopes = ( + blocked_scopes if blocked_scopes is not None else [] + ) + self._should_export_span = ( + kwargs.get("should_export_span") or is_default_export_span + ) + self._app_root_lock = threading.Lock() + self._span_export_expectation_by_id = {} + BatchSpanProcessor.__init__( + self, + span_exporter=memory_exporter, + max_export_batch_size=512, + schedule_delay_millis=1, + ) + + monkeypatch.setattr( + "langfuse._client.span_processor.LangfuseSpanProcessor.__init__", + mock_init, + ) + + client = Langfuse( + public_key="test-public-key", + secret_key="test-secret-key", + base_url="http://test-host", + tracing_enabled=True, + tracer_provider=tracer_provider, + ) + + yield client + client.flush() + + +@pytest.fixture +def get_span(memory_exporter: InMemorySpanExporter): + def _get_span(name: str) -> ReadableSpan: + for span in memory_exporter.get_finished_spans(): + if span.name == name: + return span + + raise AssertionError( + f"Span {name!r} not found in {[span.name for span in memory_exporter.get_finished_spans()]}" + ) + + return _get_span + + +@pytest.fixture +def find_spans(memory_exporter: InMemorySpanExporter): + def _find_spans(name: str) -> list[ReadableSpan]: + return [ + span for span in memory_exporter.get_finished_spans() if span.name == name + ] + + return _find_spans + + +@pytest.fixture +def json_attr(): + def _json_attr(span: ReadableSpan, attribute: str) -> Any: + value = span.attributes[attribute] + if not isinstance(value, str): + return value + + return json.loads(value) + + return _json_attr diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/e2e/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/e2e/test_batch_evaluation.py b/tests/e2e/test_batch_evaluation.py new file mode 100644 index 000000000..0632b21b8 --- /dev/null +++ b/tests/e2e/test_batch_evaluation.py @@ -0,0 +1,1139 @@ +"""Comprehensive tests for batch evaluation functionality. + +This test suite covers the run_batched_evaluation method which allows evaluating +traces, observations, and sessions fetched from Langfuse with mappers, evaluators, +and composite evaluators. +""" + +import asyncio +import time + +import pytest + +from langfuse import get_client, propagate_attributes +from langfuse.batch_evaluation import ( + BatchEvaluationResult, + BatchEvaluationResumeToken, + EvaluatorInputs, + EvaluatorStats, +) +from langfuse.experiment import Evaluation +from tests.support.utils import create_uuid, get_api, wait_for_result + +# ============================================================================ +# FIXTURES & SETUP +# ============================================================================ + + +# pytestmark = pytest.mark.skip(reason="Github CI runner overwhelmed by score volume") + + +@pytest.fixture +def langfuse_client(): + """Get a Langfuse client for testing.""" + return get_client() + + +@pytest.fixture +def sample_trace_name(): + """Generate a unique trace name for filtering.""" + return f"batch-eval-test-{create_uuid()}" + + +def _seed_trace_corpus( + *, trace_count: int = 6, tag: str | None = None +) -> tuple[str, list[str]]: + langfuse_client = get_client() + corpus_tag = tag or f"batch-eval-seed-{create_uuid()}" + trace_names: list[str] = [] + + for index in range(trace_count): + trace_name = f"{corpus_tag}-trace-{index}" + trace_names.append(trace_name) + with langfuse_client.start_as_current_observation(name=trace_name) as span: + with propagate_attributes(tags=[corpus_tag]): + span.set_trace_io( + input=f"Seed input {index}", + output=f"Seed output {index}", + ) + + langfuse_client.flush() + + filter_json = f'[{{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["{corpus_tag}"]}}]' + api = get_api(retry=False) + wait_for_result( + lambda: api.trace.list(filter=filter_json, limit=trace_count), + is_result_ready=lambda response: len(response.data) >= trace_count, + ) + + return corpus_tag, trace_names + + +@pytest.fixture(scope="module", autouse=True) +def seeded_batch_evaluation_traces(): + _seed_trace_corpus() + + +def simple_trace_mapper(*, item): + """Simple mapper for traces.""" + return EvaluatorInputs( + input=item.input if hasattr(item, "input") else None, + output=item.output if hasattr(item, "output") else None, + expected_output=None, + metadata={"trace_id": item.id}, + ) + + +def simple_evaluator(*, input, output, expected_output=None, metadata=None, **kwargs): + """Simple evaluator that returns a score based on output length.""" + if output is None: + return Evaluation(name="length_score", value=0.0, comment="No output") + + return Evaluation( + name="length_score", + value=float(len(str(output))) / 10.0, + comment=f"Length: {len(str(output))}", + ) + + +# ============================================================================ +# BASIC FUNCTIONALITY TESTS +# ============================================================================ + + +def test_run_batched_evaluation_on_observations_basic(langfuse_client): + """Test basic batch evaluation on traces.""" + result = langfuse_client.run_batched_evaluation( + scope="observations", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + max_items=1, + verbose=True, + ) + + # Validate result structure + assert isinstance(result, BatchEvaluationResult) + assert result.total_items_fetched >= 0 + assert result.total_items_processed >= 0 + assert result.total_scores_created >= 0 + assert result.completed is True + assert isinstance(result.duration_seconds, float) + assert result.duration_seconds > 0 + + # Verify evaluator stats + assert len(result.evaluator_stats) == 1 + stats = result.evaluator_stats[0] + assert isinstance(stats, EvaluatorStats) + assert stats.name == "simple_evaluator" + + +def test_run_batched_evaluation_on_traces_basic(langfuse_client): + """Test basic batch evaluation on traces.""" + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + max_items=5, + verbose=True, + ) + + # Validate result structure + assert isinstance(result, BatchEvaluationResult) + assert result.total_items_fetched >= 0 + assert result.total_items_processed >= 0 + assert result.total_scores_created >= 0 + assert result.completed is True + assert isinstance(result.duration_seconds, float) + assert result.duration_seconds > 0 + + # Verify evaluator stats + assert len(result.evaluator_stats) == 1 + stats = result.evaluator_stats[0] + assert isinstance(stats, EvaluatorStats) + assert stats.name == "simple_evaluator" + + +def test_batch_evaluation_with_filter(langfuse_client): + """Test batch evaluation with JSON filter.""" + # Create a trace with specific tag + unique_tag = f"test-filter-{create_uuid()}" + with langfuse_client.start_as_current_observation( + name=f"filtered-trace-{create_uuid()}" + ) as span: + with propagate_attributes(tags=[unique_tag]): + span.set_trace_io( + input="Filtered test", + output="Filtered output", + ) + + langfuse_client.flush() + time.sleep(3) + + # Filter format: array of filter conditions + filter_json = f'[{{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["{unique_tag}"]}}]' + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + filter=filter_json, + verbose=True, + ) + + # Should only process the filtered trace + assert result.total_items_fetched >= 1 + assert result.completed is True + + +def test_batch_evaluation_with_metadata(langfuse_client): + """Test that additional metadata is added to all scores.""" + + def metadata_checking_evaluator(*, input, output, metadata=None, **kwargs): + return Evaluation( + name="test_score", + value=1.0, + metadata={"evaluator_data": "test"}, + ) + + additional_metadata = { + "batch_run_id": "test-batch-123", + "evaluation_version": "v2.0", + } + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[metadata_checking_evaluator], + metadata=additional_metadata, + max_items=2, + ) + + assert result.total_scores_created > 0 + + # Verify scores were created with merged metadata + langfuse_client.flush() + time.sleep(3) + + # Note: In a real test, you'd verify via API that metadata was merged + # For now, just verify the operation completed + assert result.completed is True + + +def test_result_structure_fields(langfuse_client): + """Test that BatchEvaluationResult has all expected fields.""" + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + max_items=3, + ) + + # Check all result fields exist + assert hasattr(result, "total_items_fetched") + assert hasattr(result, "total_items_processed") + assert hasattr(result, "total_items_failed") + assert hasattr(result, "total_scores_created") + assert hasattr(result, "total_composite_scores_created") + assert hasattr(result, "total_evaluations_failed") + assert hasattr(result, "evaluator_stats") + assert hasattr(result, "resume_token") + assert hasattr(result, "completed") + assert hasattr(result, "duration_seconds") + assert hasattr(result, "failed_item_ids") + assert hasattr(result, "error_summary") + assert hasattr(result, "has_more_items") + assert hasattr(result, "item_evaluations") + + # Check types + assert isinstance(result.evaluator_stats, list) + assert isinstance(result.failed_item_ids, list) + assert isinstance(result.error_summary, dict) + assert isinstance(result.completed, bool) + assert isinstance(result.has_more_items, bool) + assert isinstance(result.item_evaluations, dict) + + +# ============================================================================ +# MAPPER FUNCTION TESTS +# ============================================================================ + + +def test_simple_mapper(langfuse_client): + """Test basic mapper functionality.""" + + def custom_mapper(*, item): + return EvaluatorInputs( + input=item.input if hasattr(item, "input") else "no input", + output=item.output if hasattr(item, "output") else "no output", + expected_output=None, + metadata={"custom_field": "test_value"}, + ) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=custom_mapper, + evaluators=[simple_evaluator], + max_items=2, + ) + + assert result.total_items_processed > 0 + + +@pytest.mark.asyncio +async def test_async_mapper(langfuse_client): + """Test that async mappers work correctly.""" + + async def async_mapper(*, item): + await asyncio.sleep(0.01) # Simulate async work + return EvaluatorInputs( + input=item.input if hasattr(item, "input") else None, + output=item.output if hasattr(item, "output") else None, + expected_output=None, + metadata={"async": True}, + ) + + # Note: run_batched_evaluation is synchronous but handles async mappers + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=async_mapper, + evaluators=[simple_evaluator], + max_items=2, + ) + + assert result.total_items_processed > 0 + + +def test_mapper_failure_handling(langfuse_client): + """Test that mapper failures cause items to be skipped.""" + + def failing_mapper(*, item): + raise ValueError("Intentional mapper failure") + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=failing_mapper, + evaluators=[simple_evaluator], + max_items=3, + ) + + # All items should fail due to mapper failures + assert result.total_items_failed > 0 + assert len(result.failed_item_ids) > 0 + assert "ValueError" in result.error_summary or "Exception" in result.error_summary + + +def test_mapper_with_missing_fields(langfuse_client): + """Test mapper handles traces with missing fields gracefully.""" + + def robust_mapper(*, item): + # Handle missing fields with defaults + input_val = getattr(item, "input", None) or "default_input" + output_val = getattr(item, "output", None) or "default_output" + + return EvaluatorInputs( + input=input_val, + output=output_val, + expected_output=None, + metadata={}, + ) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=robust_mapper, + evaluators=[simple_evaluator], + max_items=2, + ) + + assert result.total_items_processed > 0 + + +# ============================================================================ +# EVALUATOR TESTS +# ============================================================================ + + +def test_single_evaluator(langfuse_client): + """Test with a single evaluator.""" + + def quality_evaluator(*, input, output, **kwargs): + return Evaluation(name="quality", value=0.85, comment="High quality") + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[quality_evaluator], + max_items=2, + ) + + assert result.total_scores_created > 0 + assert len(result.evaluator_stats) == 1 + assert result.evaluator_stats[0].name == "quality_evaluator" + + +def test_multiple_evaluators(langfuse_client): + """Test with multiple evaluators running in parallel.""" + + def accuracy_evaluator(*, input, output, **kwargs): + return Evaluation(name="accuracy", value=0.9) + + def relevance_evaluator(*, input, output, **kwargs): + return Evaluation(name="relevance", value=0.8) + + def safety_evaluator(*, input, output, **kwargs): + return Evaluation(name="safety", value=1.0) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[accuracy_evaluator, relevance_evaluator, safety_evaluator], + max_items=2, + ) + + # Should have 3 evaluators + assert len(result.evaluator_stats) == 3 + assert result.total_scores_created >= result.total_items_processed * 3 + + +@pytest.mark.asyncio +async def test_async_evaluator(langfuse_client): + """Test that async evaluators work correctly.""" + + async def async_evaluator(*, input, output, **kwargs): + await asyncio.sleep(0.01) # Simulate async work + return Evaluation(name="async_score", value=0.75) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[async_evaluator], + max_items=2, + ) + + assert result.total_scores_created > 0 + + +def test_evaluator_returning_list(langfuse_client): + """Test evaluator that returns multiple Evaluations.""" + + def multi_score_evaluator(*, input, output, **kwargs): + return [ + Evaluation(name="score_1", value=0.8), + Evaluation(name="score_2", value=0.9), + Evaluation(name="score_3", value=0.7), + ] + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[multi_score_evaluator], + max_items=2, + ) + + # Should create 3 scores per item + assert result.total_scores_created >= result.total_items_processed * 3 + + +def test_evaluator_failure_statistics(langfuse_client): + """Test that evaluator failures are tracked in statistics.""" + + def working_evaluator(*, input, output, **kwargs): + return Evaluation(name="working", value=1.0) + + def failing_evaluator(*, input, output, **kwargs): + raise RuntimeError("Intentional evaluator failure") + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[working_evaluator, failing_evaluator], + max_items=3, + ) + + # Verify evaluator stats + assert len(result.evaluator_stats) == 2 + + working_stats = next( + s for s in result.evaluator_stats if s.name == "working_evaluator" + ) + assert working_stats.successful_runs > 0 + assert working_stats.failed_runs == 0 + + failing_stats = next( + s for s in result.evaluator_stats if s.name == "failing_evaluator" + ) + assert failing_stats.failed_runs > 0 + assert failing_stats.successful_runs == 0 + + # Total evaluations failed should be tracked + assert result.total_evaluations_failed > 0 + + +def test_mixed_sync_async_evaluators(langfuse_client): + """Test mixing synchronous and asynchronous evaluators.""" + + def sync_evaluator(*, input, output, **kwargs): + return Evaluation(name="sync_score", value=0.8) + + async def async_evaluator(*, input, output, **kwargs): + await asyncio.sleep(0.01) + return Evaluation(name="async_score", value=0.9) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[sync_evaluator, async_evaluator], + max_items=2, + ) + + assert len(result.evaluator_stats) == 2 + assert result.total_scores_created >= result.total_items_processed * 2 + + +# ============================================================================ +# COMPOSITE EVALUATOR TESTS +# ============================================================================ + + +def test_composite_evaluator_weighted_average(langfuse_client): + """Test composite evaluator that computes weighted average.""" + + def accuracy_evaluator(*, input, output, **kwargs): + return Evaluation(name="accuracy", value=0.8) + + def relevance_evaluator(*, input, output, **kwargs): + return Evaluation(name="relevance", value=0.9) + + def composite_evaluator(*, input, output, expected_output, metadata, evaluations): + weights = {"accuracy": 0.6, "relevance": 0.4} + total = sum( + e.value * weights.get(e.name, 0) + for e in evaluations + if isinstance(e.value, (int, float)) + ) + + return Evaluation( + name="composite_score", + value=total, + comment=f"Weighted average of {len(evaluations)} metrics", + ) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[accuracy_evaluator, relevance_evaluator], + composite_evaluator=composite_evaluator, + max_items=2, + ) + + # Should have both regular and composite scores + assert result.total_scores_created > 0 + assert result.total_composite_scores_created > 0 + assert result.total_scores_created > result.total_composite_scores_created + + +def test_composite_evaluator_pass_fail(langfuse_client): + """Test composite evaluator that implements pass/fail logic.""" + + def metric1_evaluator(*, input, output, **kwargs): + return Evaluation(name="metric1", value=0.9) + + def metric2_evaluator(*, input, output, **kwargs): + return Evaluation(name="metric2", value=0.7) + + def pass_fail_composite(*, input, output, expected_output, metadata, evaluations): + thresholds = {"metric1": 0.8, "metric2": 0.6} + + passes = all( + e.value >= thresholds.get(e.name, 0) + for e in evaluations + if isinstance(e.value, (int, float)) + ) + + return Evaluation( + name="passes_all_checks", + value=1.0 if passes else 0.0, + comment="All checks passed" if passes else "Some checks failed", + ) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[metric1_evaluator, metric2_evaluator], + composite_evaluator=pass_fail_composite, + max_items=2, + ) + + assert result.total_composite_scores_created > 0 + + +@pytest.mark.asyncio +async def test_async_composite_evaluator(langfuse_client): + """Test async composite evaluator.""" + + def evaluator1(*, input, output, **kwargs): + return Evaluation(name="eval1", value=0.8) + + async def async_composite(*, input, output, expected_output, metadata, evaluations): + await asyncio.sleep(0.01) # Simulate async processing + avg = sum( + e.value for e in evaluations if isinstance(e.value, (int, float)) + ) / len(evaluations) + return Evaluation(name="async_composite", value=avg) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[evaluator1], + composite_evaluator=async_composite, + max_items=2, + ) + + assert result.total_composite_scores_created > 0 + + +def test_composite_evaluator_with_no_evaluations(langfuse_client): + """Test composite evaluator when no evaluations are present.""" + + def always_failing_evaluator(*, input, output, **kwargs): + raise Exception("Always fails") + + def composite_evaluator(*, input, output, expected_output, metadata, evaluations): + # Should not be called if no evaluations succeed + return Evaluation(name="composite", value=0.0) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[always_failing_evaluator], + composite_evaluator=composite_evaluator, + max_items=2, + ) + + # Composite evaluator should not create scores if no evaluations + assert result.total_composite_scores_created == 0 + + +def test_composite_evaluator_failure_handling(langfuse_client): + """Test that composite evaluator failures are handled gracefully.""" + + def evaluator1(*, input, output, **kwargs): + return Evaluation(name="eval1", value=0.8) + + def failing_composite(*, input, output, expected_output, metadata, evaluations): + raise ValueError("Composite evaluator failed") + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[evaluator1], + composite_evaluator=failing_composite, + max_items=2, + ) + + # Regular scores should still be created + assert result.total_scores_created > 0 + # But no composite scores + assert result.total_composite_scores_created == 0 + + +# ============================================================================ +# ERROR HANDLING TESTS +# ============================================================================ + + +def test_mapper_failure_skips_item(langfuse_client): + """Test that mapper failure causes item to be skipped.""" + + call_count = {"count": 0} + + def sometimes_failing_mapper(*, item): + call_count["count"] += 1 + if call_count["count"] % 2 == 0: + raise Exception("Mapper failed") + return simple_trace_mapper(item=item) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=sometimes_failing_mapper, + evaluators=[simple_evaluator], + max_items=4, + ) + + # Some items should fail, some should succeed + assert result.total_items_failed > 0 + assert result.total_items_processed > 0 + + +def test_evaluator_failure_continues(langfuse_client): + """Test that one evaluator failing doesn't stop others.""" + + def working_evaluator1(*, input, output, **kwargs): + return Evaluation(name="working1", value=0.8) + + def failing_evaluator(*, input, output, **kwargs): + raise Exception("Evaluator failed") + + def working_evaluator2(*, input, output, **kwargs): + return Evaluation(name="working2", value=0.9) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[working_evaluator1, failing_evaluator, working_evaluator2], + max_items=2, + ) + + # Working evaluators should still create scores + assert result.total_scores_created >= result.total_items_processed * 2 + + # Failing evaluator should be tracked + failing_stats = next( + s for s in result.evaluator_stats if s.name == "failing_evaluator" + ) + assert failing_stats.failed_runs > 0 + + +def test_all_evaluators_fail(langfuse_client): + """Test when all evaluators fail but item is still processed.""" + + def failing_evaluator1(*, input, output, **kwargs): + raise Exception("Failed 1") + + def failing_evaluator2(*, input, output, **kwargs): + raise Exception("Failed 2") + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[failing_evaluator1, failing_evaluator2], + max_items=2, + ) + + # Items should be processed even if all evaluators fail + assert result.total_items_processed > 0 + # But no scores created + assert result.total_scores_created == 0 + # All evaluations failed + assert result.total_evaluations_failed > 0 + + +# ============================================================================ +# EDGE CASES TESTS +# ============================================================================ + + +def test_empty_results_handling(langfuse_client): + """Test batch evaluation when filter returns no items.""" + nonexistent_name = f"nonexistent-trace-{create_uuid()}" + nonexistent_filter = f'[{{"type": "string", "column": "name", "operator": "=", "value": "{nonexistent_name}"}}]' + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + filter=nonexistent_filter, + ) + + assert result.total_items_fetched == 0 + assert result.total_items_processed == 0 + assert result.total_scores_created == 0 + assert result.completed is True + assert result.has_more_items is False + + +def test_max_items_zero(langfuse_client): + """Test with max_items=0 (should process no items).""" + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + max_items=0, + ) + + assert result.total_items_fetched == 0 + assert result.total_items_processed == 0 + + +def test_evaluation_value_type_conversions(langfuse_client): + """Test that different evaluation value types are handled correctly.""" + + def multi_type_evaluator(*, input, output, **kwargs): + return [ + Evaluation(name="int_score", value=5), # int + Evaluation(name="float_score", value=0.85), # float + Evaluation(name="bool_score", value=True), # bool + Evaluation(name="none_score", value=None), # None + ] + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[multi_type_evaluator], + max_items=1, + ) + + # All value types should be converted and scores created + assert result.total_scores_created >= 4 + + +# ============================================================================ +# PAGINATION TESTS +# ============================================================================ + + +def test_pagination_with_max_items(langfuse_client): + """Test that max_items limit is respected.""" + # Create more traces to ensure we have enough data + for i in range(10): + with langfuse_client.start_as_current_observation( + name=f"pagination-test-{create_uuid()}" + ) as span: + with propagate_attributes(tags=["pagination_test"]): + span.set_trace_io( + input=f"Input {i}", + output=f"Output {i}", + ) + + langfuse_client.flush() + time.sleep(3) + + filter_json = '[{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["pagination_test"]}]' + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + filter=filter_json, + max_items=5, + fetch_batch_size=2, + ) + + # Should not exceed max_items + assert result.total_items_processed <= 5 + + +def test_has_more_items_flag(langfuse_client): + """Test that has_more_items flag is set correctly when max_items is reached.""" + # Create enough traces to exceed max_items + batch_tag = f"batch-test-{create_uuid()}" + for i in range(15): + with langfuse_client.start_as_current_observation( + name=f"more-items-test-{i}" + ) as span: + with propagate_attributes(tags=[batch_tag]): + span.set_trace_io( + input=f"Input {i}", + output=f"Output {i}", + ) + + langfuse_client.flush() + time.sleep(3) + + filter_json = f'[{{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["{batch_tag}"]}}]' + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + filter=filter_json, + max_items=5, + fetch_batch_size=2, + ) + + # has_more_items should be True if we hit the limit + if result.total_items_fetched >= 5: + assert result.has_more_items is True + + +def test_fetch_batch_size_parameter(langfuse_client): + """Test that different fetch_batch_size values work correctly.""" + for batch_size in [1, 5, 10]: + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + max_items=3, + fetch_batch_size=batch_size, + ) + + # Should complete regardless of batch size + assert result.completed is True or result.total_items_processed > 0 + + +# ============================================================================ +# RESUME FUNCTIONALITY TESTS +# ============================================================================ + + +def test_resume_token_structure(langfuse_client): + """Test that BatchEvaluationResumeToken has correct structure.""" + resume_token = BatchEvaluationResumeToken( + scope="traces", + filter='{"test": "filter"}', + last_processed_timestamp="2024-01-01T00:00:00Z", + last_processed_id="trace-123", + items_processed=10, + ) + + assert resume_token.scope == "traces" + assert resume_token.filter == '{"test": "filter"}' + assert resume_token.last_processed_timestamp == "2024-01-01T00:00:00Z" + assert resume_token.last_processed_id == "trace-123" + assert resume_token.items_processed == 10 + + +# ============================================================================ +# CONCURRENCY TESTS +# ============================================================================ + + +def test_max_concurrency_parameter(langfuse_client): + """Test that max_concurrency parameter works correctly.""" + for concurrency in [1, 5, 10]: + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + max_items=3, + max_concurrency=concurrency, + ) + + # Should complete regardless of concurrency + assert result.completed is True or result.total_items_processed > 0 + + +# ============================================================================ +# STATISTICS TESTS +# ============================================================================ + + +def test_evaluator_stats_structure(langfuse_client): + """Test that EvaluatorStats has correct structure.""" + + def test_evaluator(*, input, output, **kwargs): + return Evaluation(name="test", value=1.0) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[test_evaluator], + max_items=2, + ) + + assert len(result.evaluator_stats) == 1 + stats = result.evaluator_stats[0] + + # Check all fields exist + assert hasattr(stats, "name") + assert hasattr(stats, "total_runs") + assert hasattr(stats, "successful_runs") + assert hasattr(stats, "failed_runs") + assert hasattr(stats, "total_scores_created") + + # Check values + assert stats.name == "test_evaluator" + assert stats.total_runs == result.total_items_processed + assert stats.successful_runs == result.total_items_processed + assert stats.failed_runs == 0 + + +def test_evaluator_stats_tracking(langfuse_client): + """Test that evaluator statistics are tracked correctly.""" + + call_count = {"count": 0} + + def sometimes_failing_evaluator(*, input, output, **kwargs): + call_count["count"] += 1 + if call_count["count"] % 2 == 0: + raise Exception("Failed") + return Evaluation(name="test", value=1.0) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[sometimes_failing_evaluator], + max_items=4, + ) + + stats = result.evaluator_stats[0] + assert stats.total_runs == result.total_items_processed + assert stats.successful_runs > 0 + assert stats.failed_runs > 0 + assert stats.successful_runs + stats.failed_runs == stats.total_runs + + +def test_error_summary_aggregation(langfuse_client): + """Test that error types are aggregated correctly in error_summary.""" + + def failing_mapper(*, item): + raise ValueError("Mapper error") + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=failing_mapper, + evaluators=[simple_evaluator], + max_items=3, + ) + + # Error summary should contain the error type + assert len(result.error_summary) > 0 + assert any("Error" in key for key in result.error_summary.keys()) + + +def test_failed_item_ids_collected(langfuse_client): + """Test that failed item IDs are collected.""" + + def failing_mapper(*, item): + raise Exception("Failed") + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=failing_mapper, + evaluators=[simple_evaluator], + max_items=3, + ) + + assert len(result.failed_item_ids) > 0 + # Each failed ID should be a string + assert all(isinstance(item_id, str) for item_id in result.failed_item_ids) + + +# ============================================================================ +# PERFORMANCE TESTS +# ============================================================================ + + +def test_duration_tracking(langfuse_client): + """Test that duration is tracked correctly.""" + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + max_items=2, + ) + + assert result.duration_seconds > 0 + assert result.duration_seconds < 60 # Should complete quickly for small batch + + +def test_verbose_logging(langfuse_client): + """Test that verbose=True doesn't cause errors.""" + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[simple_evaluator], + max_items=2, + verbose=True, # Should log progress + ) + + assert result.completed is True + + +# ============================================================================ +# ITEM EVALUATIONS TESTS +# ============================================================================ + + +def test_item_evaluations_basic(langfuse_client): + """Test that item_evaluations dict contains correct structure.""" + + def test_evaluator(*, input, output, **kwargs): + return Evaluation(name="test_metric", value=0.5) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[test_evaluator], + max_items=3, + ) + + # Check that item_evaluations is a dict + assert isinstance(result.item_evaluations, dict) + + # Should have evaluations for each processed item + assert len(result.item_evaluations) == result.total_items_processed + + # Each entry should be a list of Evaluation objects + for item_id, evaluations in result.item_evaluations.items(): + assert isinstance(item_id, str) + assert isinstance(evaluations, list) + assert all(isinstance(e, Evaluation) for e in evaluations) + # Should have one evaluation per evaluator + assert len(evaluations) == 1 + assert evaluations[0].name == "test_metric" + + +def test_item_evaluations_multiple_evaluators(langfuse_client): + """Test item_evaluations with multiple evaluators.""" + + def accuracy_evaluator(*, input, output, **kwargs): + return Evaluation(name="accuracy", value=0.8) + + def relevance_evaluator(*, input, output, **kwargs): + return Evaluation(name="relevance", value=0.9) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[accuracy_evaluator, relevance_evaluator], + max_items=2, + ) + + # Check structure + assert len(result.item_evaluations) == result.total_items_processed + + # Each item should have evaluations from both evaluators + for item_id, evaluations in result.item_evaluations.items(): + assert len(evaluations) == 2 + eval_names = {e.name for e in evaluations} + assert eval_names == {"accuracy", "relevance"} + + +def test_item_evaluations_with_composite(langfuse_client): + """Test that item_evaluations includes composite evaluations.""" + + def base_evaluator(*, input, output, **kwargs): + return Evaluation(name="base_score", value=0.7) + + def composite_evaluator(*, input, output, expected_output, metadata, evaluations): + return Evaluation( + name="composite_score", + value=sum( + e.value for e in evaluations if isinstance(e.value, (int, float)) + ), + ) + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=simple_trace_mapper, + evaluators=[base_evaluator], + composite_evaluator=composite_evaluator, + max_items=2, + ) + + # Each item should have both base and composite evaluations + for item_id, evaluations in result.item_evaluations.items(): + assert len(evaluations) == 2 + eval_names = {e.name for e in evaluations} + assert eval_names == {"base_score", "composite_score"} + + # Verify composite scores were created + assert result.total_composite_scores_created > 0 + + +def test_item_evaluations_empty_on_failure(langfuse_client): + """Test that failed items don't appear in item_evaluations.""" + + def failing_mapper(*, item): + raise Exception("Mapper failed") + + result = langfuse_client.run_batched_evaluation( + scope="traces", + mapper=failing_mapper, + evaluators=[simple_evaluator], + max_items=3, + ) + + # All items failed, so item_evaluations should be empty + assert len(result.item_evaluations) == 0 + assert result.total_items_failed > 0 diff --git a/tests/test_core_sdk.py b/tests/e2e/test_core_sdk.py similarity index 54% rename from tests/test_core_sdk.py rename to tests/e2e/test_core_sdk.py index 9d1acae85..614d6da41 100644 --- a/tests/test_core_sdk.py +++ b/tests/e2e/test_core_sdk.py @@ -1,18 +1,21 @@ import os import time from asyncio import gather -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from time import sleep import pytest +from tenacity import Retrying, stop_after_delay, wait_fixed -from langfuse import Langfuse +from langfuse import Langfuse, propagate_attributes from langfuse._client.resource_manager import LangfuseResourceManager from langfuse._utils import _get_timestamp -from tests.api_wrapper import LangfuseAPI -from tests.utils import ( +from tests.support.api_wrapper import LangfuseAPI +from tests.support.utils import ( create_uuid, get_api, + wait_for_result, + wait_for_trace, ) @@ -22,18 +25,18 @@ async def test_concurrency(): async def update_generation(i, langfuse: Langfuse): # Create a new trace with a generation - with langfuse.start_as_current_span(name=f"parent-{i}") as parent_span: - # Set trace name - parent_span.update_trace(name=str(i)) + with langfuse.start_as_current_observation(name=f"parent-{i}"): + with propagate_attributes(trace_name=str(i)): + # Create generation as a child + generation = langfuse.start_observation( + as_type="generation", name=str(i) + ) - # Create generation as a child - generation = langfuse.start_generation(name=str(i)) + # Update generation with metadata + generation.update(metadata={"count": str(i)}) - # Update generation with metadata - generation.update(metadata={"count": str(i)}) - - # End the generation - generation.end() + # End the generation + generation.end() # Create Langfuse client langfuse = Langfuse() @@ -50,7 +53,7 @@ async def update_generation(i, langfuse: Langfuse): api = get_api() for i in range(100): # Find the observations with the expected name - observations = api.observations.get_many(name=str(i)).data + observations = api.legacy.observations_v1.get_many(name=str(i)).data # Find generation observations (there should be at least one) generation_obs = [obs for obs in observations if obs.type == "GENERATION"] @@ -68,11 +71,11 @@ def test_flush(): trace_ids = [] for i in range(2): - # Create spans and set the trace name using update_trace - with langfuse.start_as_current_span(name="span-" + str(i)) as span: - span.update_trace(name=str(i)) - # Store the trace ID for later verification - trace_ids.append(langfuse.get_current_trace_id()) + # Create spans and set the trace name using propagate_attributes + with langfuse.start_as_current_observation(name="span-" + str(i)): + with propagate_attributes(trace_name=str(i)): + # Store the trace ID for later verification + trace_ids.append(langfuse.get_current_trace_id()) # Flush all pending spans to the Langfuse API langfuse.flush() @@ -91,14 +94,14 @@ def test_invalid_score_data_does_not_raise_exception(): langfuse = Langfuse() # Create a span and set trace properties - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace( - name="this-is-so-great-new", + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name="this-is-so-great-new", user_id="test", - metadata="test", - ) - # Get trace ID for later use - trace_id = span.trace_id + metadata={"test": "test"}, + ): + # Get trace ID for later use + trace_id = span.trace_id # Ensure data is sent langfuse.flush() @@ -118,19 +121,62 @@ def test_invalid_score_data_does_not_raise_exception(): # We can't assert queue size in OTEL implementation, but we can verify it completes without exception +def test_create_session_score(): + langfuse = Langfuse() + + session_id = "my-session" + + # Create a span and set trace properties + with langfuse.start_as_current_observation(name="test-span"): + with propagate_attributes( + trace_name="this-is-so-great-new", + user_id="test", + metadata={"test": "test"}, + session_id=session_id, + ): + pass + + # Ensure data is sent + langfuse.flush() + sleep(2) + + # Create a numeric score + score_id = create_uuid() + + langfuse.create_score( + score_id=score_id, + session_id=session_id, + name="this-is-a-score", + value=1, + ) + + # Ensure data is sent + langfuse.flush() + sleep(2) + + # Retrieve and verify + score = get_api().scores.get_by_id(score_id) + + # find the score by name (server may transform the id format) + assert score is not None + assert score.value == 1 + assert score.data_type == "NUMERIC" + assert score.session_id == session_id + + def test_create_numeric_score(): langfuse = Langfuse() api_wrapper = LangfuseAPI() # Create a span and set trace properties - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace( - name="this-is-so-great-new", + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name="this-is-so-great-new", user_id="test", - metadata="test", - ) - # Get trace ID for later use - trace_id = span.trace_id + metadata={"test": "test"}, + ): + # Get trace ID for later use + trace_id = span.trace_id # Ensure data is sent langfuse.flush() @@ -146,8 +192,11 @@ def test_create_numeric_score(): ) # Create a generation in the same trace - generation = langfuse.start_generation( - name="yet another child", metadata="test", trace_context={"trace_id": trace_id} + generation = langfuse.start_observation( + as_type="generation", + name="yet another child", + metadata="test", + trace_context={"trace_id": trace_id}, ) generation.end() @@ -158,10 +207,12 @@ def test_create_numeric_score(): # Retrieve and verify trace = api_wrapper.get_trace(trace_id) - assert trace["scores"][0]["id"] == score_id - assert trace["scores"][0]["value"] == 1 - assert trace["scores"][0]["dataType"] == "NUMERIC" - assert trace["scores"][0]["stringValue"] is None + # Find the score by name (server may transform the ID format) + score = next((s for s in trace["scores"] if s["name"] == "this-is-a-score"), None) + assert score is not None + assert score["value"] == 1 + assert score["dataType"] == "NUMERIC" + assert score["stringValue"] is None def test_create_boolean_score(): @@ -169,18 +220,18 @@ def test_create_boolean_score(): api_wrapper = LangfuseAPI() # Create a span and set trace properties - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace( - name="this-is-so-great-new", + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name="this-is-so-great-new", user_id="test", - metadata="test", - ) - # Get trace ID for later use - trace_id = span.trace_id + metadata={"test": "test"}, + ): + # Get trace ID for later use + trace_id = span.trace_id # Ensure data is sent langfuse.flush() - sleep(2) + api_wrapper.get_trace(trace_id) # Create a boolean score score_id = create_uuid() @@ -193,22 +244,34 @@ def test_create_boolean_score(): ) # Create a generation in the same trace - generation = langfuse.start_generation( - name="yet another child", metadata="test", trace_context={"trace_id": trace_id} + generation = langfuse.start_observation( + as_type="generation", + name="yet another child", + metadata="test", + trace_context={"trace_id": trace_id}, ) generation.end() # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) + trace = api_wrapper.get_trace( + trace_id, + is_result_ready=lambda trace: any( + score["name"] == "this-is-a-score" for score in trace.get("scores", []) + ), + ) - assert trace["scores"][0]["id"] == score_id - assert trace["scores"][0]["dataType"] == "BOOLEAN" - assert trace["scores"][0]["value"] == 1 - assert trace["scores"][0]["stringValue"] == "True" + # Find the score we created by name + created_score = next( + (s for s in trace["scores"] if s["name"] == "this-is-a-score"), None + ) + assert created_score is not None, "Score not found in trace" + assert created_score["id"] == score_id + assert created_score["dataType"] == "BOOLEAN" + assert created_score["value"] == 1 + assert created_score["stringValue"] == "True" def test_create_categorical_score(): @@ -216,18 +279,18 @@ def test_create_categorical_score(): api_wrapper = LangfuseAPI() # Create a span and set trace properties - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace( - name="this-is-so-great-new", + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name="this-is-so-great-new", user_id="test", - metadata="test", - ) - # Get trace ID for later use - trace_id = span.trace_id + metadata={"test": "test"}, + ): + # Get trace ID for later use + trace_id = span.trace_id # Ensure data is sent langfuse.flush() - sleep(2) + api_wrapper.get_trace(trace_id) # Create a categorical score score_id = create_uuid() @@ -239,46 +302,191 @@ def test_create_categorical_score(): ) # Create a generation in the same trace - generation = langfuse.start_generation( - name="yet another child", metadata="test", trace_context={"trace_id": trace_id} + generation = langfuse.start_observation( + as_type="generation", + name="yet another child", + metadata="test", + trace_context={"trace_id": trace_id}, ) generation.end() + # Ensure data is sent + langfuse.flush() + + # Retrieve and verify + trace = api_wrapper.get_trace( + trace_id, + is_result_ready=lambda trace: any( + score["name"] == "this-is-a-score" for score in trace.get("scores", []) + ), + ) + + # Find the score we created by name + created_score = next( + (s for s in trace["scores"] if s["name"] == "this-is-a-score"), None + ) + assert created_score is not None, "Score not found in trace" + assert created_score["id"] == score_id + assert created_score["dataType"] == "CATEGORICAL" + assert created_score["value"] == 0 + assert created_score["stringValue"] == "high score" + + +def test_create_text_score(): + langfuse = Langfuse() + api_wrapper = LangfuseAPI() + + # Create a span and set trace properties + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name="this-is-so-great-new", + user_id="test", + metadata={"test": "test"}, + ): + # Get trace ID for later use + trace_id = span.trace_id + # Ensure data is sent langfuse.flush() sleep(2) + # Create a text score + score_id = create_uuid() + langfuse.create_score( + score_id=score_id, + trace_id=trace_id, + name="this-is-a-score", + value="This is a detailed text evaluation of the output quality.", + data_type="TEXT", + ) + + # Create a generation in the same trace + generation = langfuse.start_observation( + as_type="generation", + name="yet another child", + metadata="test", + trace_context={"trace_id": trace_id}, + ) + generation.end() + + # Ensure data is sent + langfuse.flush() + + # Retrieve and verify with retry + for attempt in Retrying( + stop=stop_after_delay(10), wait=wait_fixed(0.1), reraise=True + ): + with attempt: + trace = api_wrapper.get_trace(trace_id) + + # Find the score we created by name + created_score = next( + (s for s in trace["scores"] if s["name"] == "this-is-a-score"), None + ) + assert created_score is not None, "Score not found in trace" + assert created_score["id"] == score_id + assert created_score["dataType"] == "TEXT" + + assert ( + created_score["stringValue"] + == "This is a detailed text evaluation of the output quality." + ) + + +def test_create_score_with_custom_timestamp(): + langfuse = Langfuse() + api_wrapper = LangfuseAPI() + + # Create a span and set trace properties + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name="test-custom-timestamp", + user_id="test", + metadata={"test": "test"}, + ): + # Get trace ID for later use + trace_id = span.trace_id + + # Ensure data is sent + langfuse.flush() + api_wrapper.get_trace(trace_id) + + custom_timestamp = datetime.now(timezone.utc) - timedelta(hours=1) + score_id = create_uuid() + langfuse.create_score( + score_id=score_id, + trace_id=trace_id, + name="custom-timestamp-score", + value=0.85, + data_type="NUMERIC", + timestamp=custom_timestamp, + ) + + # Ensure data is sent + langfuse.flush() + # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) + trace = api_wrapper.get_trace( + trace_id, + is_result_ready=lambda trace: any( + score["name"] == "custom-timestamp-score" + for score in trace.get("scores", []) + ), + ) - assert trace["scores"][0]["id"] == score_id - assert trace["scores"][0]["dataType"] == "CATEGORICAL" - assert trace["scores"][0]["value"] == 0 - assert trace["scores"][0]["stringValue"] == "high score" + # Find the score we created by name + created_score = next( + (s for s in trace["scores"] if s["name"] == "custom-timestamp-score"), None + ) + assert created_score is not None, "Score not found in trace" + assert created_score["id"] == score_id + assert created_score["dataType"] == "NUMERIC" + assert created_score["value"] == 0.85 + + # Verify timestamp is close to our custom timestamp + # Parse the timestamp from the API response + response_timestamp = datetime.fromisoformat( + created_score["timestamp"].replace("Z", "+00:00") + ) + + # Check that the timestamps are within 1 second of each other + # (allowing for some processing time and rounding) + time_diff = abs((response_timestamp - custom_timestamp).total_seconds()) + assert time_diff < 1, ( + f"Timestamp difference too large: {time_diff}s. Expected < 1s. Custom: {custom_timestamp}, Response: {response_timestamp}" + ) def test_create_trace(): langfuse = Langfuse() trace_name = create_uuid() - # Create a span and update the trace properties - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace( - name=trace_name, + # Create a span and set the trace properties using propagate_attributes + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name=trace_name, user_id="test", metadata={"key": "value"}, tags=["tag1", "tag2"], - public=True, - ) - # Get trace ID for later verification - trace_id = langfuse.get_current_trace_id() + ): + span.set_trace_as_public() + # Get trace ID for later verification + trace_id = langfuse.get_current_trace_id() # Ensure data is sent to the API langfuse.flush() - sleep(2) # Retrieve the trace from the API - trace = LangfuseAPI().get_trace(trace_id) + trace = LangfuseAPI().get_trace( + trace_id, + is_result_ready=lambda trace: ( + trace.get("name") == trace_name + and trace.get("userId") == "test" + and trace.get("metadata", {}).get("key") == "value" + and trace.get("tags") == ["tag1", "tag2"] + and trace.get("public") is True + ), + ) # Verify all trace properties assert trace["name"] == trace_name @@ -295,43 +503,95 @@ def test_create_update_trace(): trace_name = create_uuid() # Create initial span with trace properties - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace( - name=trace_name, + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name=trace_name, user_id="test", metadata={"key": "value"}, - public=True, - ) - # Get trace ID for later reference - trace_id = span.trace_id + ): + span.set_trace_as_public() + # Get trace ID for later reference + trace_id = span.trace_id - # Allow a small delay before updating - sleep(1) + # Allow a small delay before updating + sleep(1) - # Update trace properties - span.update_trace(metadata={"key2": "value2"}, public=False) + # Update trace properties with additional metadata + with propagate_attributes(metadata={"key2": "value2"}): + pass # Metadata update only, set_trace_as_public is one-way + + # Ensure data is sent to the API + langfuse.flush() + + assert isinstance(trace_id, str) + # Retrieve and verify trace + trace = wait_for_trace( + trace_id, + is_result_ready=lambda trace: ( + trace.name == trace_name + and trace.user_id == "test" + and trace.metadata is not None + and trace.metadata.get("key") == "value" + and trace.metadata.get("key2") == "value2" + and trace.public is True + ), + ) + + assert trace.name == trace_name + assert trace.user_id == "test" + assert trace.metadata["key"] == "value" + assert trace.metadata["key2"] == "value2" + assert trace.public is True + + +def test_create_update_current_trace(): + langfuse = Langfuse() + + trace_name = create_uuid() + + # Create initial span with trace properties using propagate_attributes and set_current_trace_io + with langfuse.start_as_current_observation(name="test-span-current") as span: + with propagate_attributes( + trace_name=trace_name, + user_id="test", + metadata={"key": "value"}, + ): + langfuse.set_current_trace_io(input="test_input") + langfuse.set_current_trace_as_public() + # Get trace ID for later reference + trace_id = span.trace_id + + # Allow a small delay before updating + sleep(1) + + # Update trace properties with additional metadata and version + with propagate_attributes(metadata={"key2": "value2"}, version="1.0"): + pass # Metadata update only, publish is one-way # Ensure data is sent to the API langfuse.flush() sleep(2) - # Ensure trace_id is a string before passing to the API - if trace_id is not None: - # Retrieve and verify trace - trace = get_api().trace.get(trace_id) + assert isinstance(trace_id, str) + # Retrieve and verify trace + trace = get_api().trace.get(trace_id) - assert trace.name == trace_name - assert trace.user_id == "test" - assert trace.metadata["key"] == "value" - assert trace.metadata["key2"] == "value2" - assert trace.public is False + # The 2nd update to the trace must not erase previously set attributes + assert trace.name == trace_name + assert trace.user_id == "test" + assert trace.metadata["key"] == "value" + assert trace.metadata["key2"] == "value2" + assert trace.public is True + assert trace.version == "1.0" + assert trace.input == "test_input" def test_create_generation(): langfuse = Langfuse() # Create a generation using OTEL approach - generation = langfuse.start_generation( + generation = langfuse.start_observation( + as_type="generation", name="query-generation", model="gpt-3.5-turbo-0125", model_parameters={ @@ -430,7 +690,8 @@ def test_create_generation_complex( ): langfuse = Langfuse() - generation = langfuse.start_generation( + generation = langfuse.start_observation( + as_type="generation", name="query-generation", input=[ {"role": "system", "content": "You are a helpful assistant."}, @@ -482,7 +743,7 @@ def test_create_span(): langfuse = Langfuse() # Create span using OTEL-based client - span = langfuse.start_span( + span = langfuse.start_observation( name="span", input={"key": "value"}, output={"key": "value"}, @@ -528,18 +789,17 @@ def test_score_trace(): trace_name = create_uuid() # Create a span and set trace name - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace(name=trace_name) - - # Get trace ID for later verification - trace_id = langfuse.get_current_trace_id() - - # Create score for the trace - langfuse.score_current_trace( - name="valuation", - value=0.5, - comment="This is a comment", - ) + with langfuse.start_as_current_observation(name="test-span"): + with propagate_attributes(trace_name=trace_name): + # Get trace ID for later verification + trace_id = langfuse.get_current_trace_id() + + # Create score for the trace + langfuse.score_current_trace( + name="valuation", + value=0.5, + comment="This is a comment", + ) # Ensure data is sent langfuse.flush() @@ -549,11 +809,10 @@ def test_score_trace(): trace = api_wrapper.get_trace(trace_id) assert trace["name"] == trace_name - assert len(trace["scores"]) == 1 - - score = trace["scores"][0] - assert score["name"] == "valuation" + # Find the score we created by name (server may create additional auto-scores) + score = next((s for s in trace["scores"] if s["name"] == "valuation"), None) + assert score is not None assert score["value"] == 0.5 assert score["comment"] == "This is a comment" assert score["observationId"] is None @@ -566,19 +825,17 @@ def test_score_trace_nested_trace(): trace_name = create_uuid() # Create a trace with span - with langfuse.start_as_current_span(name="test-span") as span: - # Set trace name - span.update_trace(name=trace_name) - - # Score using the span's method for scoring the trace - span.score_trace( - name="valuation", - value=0.5, - comment="This is a comment", - ) - - # Get trace ID for verification - trace_id = span.trace_id + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes(trace_name=trace_name): + # Score using the span's method for scoring the trace + span.score_trace( + name="valuation", + value=0.5, + comment="This is a comment", + ) + + # Get trace ID for verification + trace_id = span.trace_id # Ensure data is sent langfuse.flush() @@ -588,11 +845,10 @@ def test_score_trace_nested_trace(): trace = get_api().trace.get(trace_id) assert trace.name == trace_name - assert len(trace.scores) == 1 - - score = trace.scores[0] - assert score.name == "valuation" + # Find the score we created by name (server may create additional auto-scores) + score = next((s for s in trace.scores if s.name == "valuation"), None) + assert score is not None assert score.value == 0.5 assert score.comment == "This is a comment" assert score.observation_id is None # API returns this field name @@ -605,25 +861,24 @@ def test_score_trace_nested_observation(): trace_name = create_uuid() # Create a parent span and set trace name - with langfuse.start_as_current_span(name="parent-span") as parent_span: - parent_span.update_trace(name=trace_name) + with langfuse.start_as_current_observation(name="parent-span") as parent_span: + with propagate_attributes(trace_name=trace_name): + # Create a child span + child_span = langfuse.start_observation(name="span") - # Create a child span - child_span = langfuse.start_span(name="span") + # Score the child span + child_span.score( + name="valuation", + value=0.5, + comment="This is a comment", + ) - # Score the child span - child_span.score( - name="valuation", - value=0.5, - comment="This is a comment", - ) - - # Get IDs for verification - child_span_id = child_span.id - trace_id = parent_span.trace_id + # Get IDs for verification + child_span_id = child_span.id + trace_id = parent_span.trace_id - # End the child span - child_span.end() + # End the child span + child_span.end() # Ensure data is sent langfuse.flush() @@ -633,11 +888,10 @@ def test_score_trace_nested_observation(): trace = get_api().trace.get(trace_id) assert trace.name == trace_name - assert len(trace.scores) == 1 - - score = trace.scores[0] - assert score.name == "valuation" + # Find the score we created by name (server may create additional auto-scores) + score = next((s for s in trace.scores if s.name == "valuation"), None) + assert score is not None assert score.value == 0.5 assert score.comment == "This is a comment" assert score.observation_id == child_span_id # API returns this field name @@ -649,7 +903,7 @@ def test_score_span(): api_wrapper = LangfuseAPI() # Create a span - span = langfuse.start_span( + span = langfuse.start_observation( name="span", input={"key": "value"}, output={"key": "value"}, @@ -679,12 +933,11 @@ def test_score_span(): # Retrieve and verify trace = api_wrapper.get_trace(trace_id) - assert len(trace["scores"]) == 1 assert len(trace["observations"]) == 1 - score = trace["scores"][0] - - assert score["name"] == "valuation" + # Find the score we created by name (server may create additional auto-scores) + score = next((s for s in trace["scores"] if s["name"] == "valuation"), None) + assert score is not None assert score["value"] == 1 assert score["comment"] == "This is a comment" assert score["observationId"] == span_id @@ -697,17 +950,16 @@ def test_create_trace_and_span(): trace_name = create_uuid() # Create parent span and set trace name - with langfuse.start_as_current_span(name=trace_name) as parent_span: - parent_span.update_trace(name=trace_name) - - # Create a child span - child_span = parent_span.start_span(name="span") + with langfuse.start_as_current_observation(name=trace_name) as parent_span: + with propagate_attributes(trace_name=trace_name): + # Create a child span + child_span = parent_span.start_observation(name="span") - # Get trace ID for verification - trace_id = parent_span.trace_id + # Get trace ID for verification + trace_id = parent_span.trace_id - # End the child span - child_span.end() + # End the child span + child_span.end() # Ensure data is sent langfuse.flush() @@ -735,19 +987,20 @@ def test_create_trace_and_generation(): trace_name = create_uuid() # Create parent span and set trace properties - with langfuse.start_as_current_span(name=trace_name) as parent_span: - parent_span.update_trace( - name=trace_name, input={"key": "value"}, session_id="test-session-id" - ) + with langfuse.start_as_current_observation(name=trace_name) as parent_span: + with propagate_attributes(trace_name=trace_name, session_id="test-session-id"): + parent_span.set_trace_io(input={"key": "value"}) - # Create a generation as child - generation = parent_span.start_generation(name="generation") + # Create a generation as child + generation = parent_span.start_observation( + as_type="generation", name="generation" + ) - # Get IDs for verification - trace_id = parent_span.trace_id + # Get IDs for verification + trace_id = parent_span.trace_id - # End the generation - generation.end() + # End the generation + generation.end() # Ensure data is sent langfuse.flush() @@ -787,8 +1040,10 @@ def test_create_generation_and_trace(): trace_context = {"trace_id": langfuse.create_trace_id()} # Create a generation with this context - generation = langfuse.start_generation( - name="generation", trace_context=trace_context + generation = langfuse.start_observation( + as_type="generation", + name="generation", + trace_context=trace_context, ) # Get trace ID for verification @@ -800,10 +1055,11 @@ def test_create_generation_and_trace(): sleep(0.1) # Update trace properties in a separate span - with langfuse.start_as_current_span( + with langfuse.start_as_current_observation( name="trace-update", trace_context={"trace_id": trace_id} - ) as span: - span.update_trace(name=trace_name) + ): + with propagate_attributes(trace_name=trace_name): + pass # Ensure data is sent langfuse.flush() @@ -830,7 +1086,7 @@ def test_create_span_and_get_observation(): langfuse = Langfuse() # Create span - span = langfuse.start_span(name="span") + span = langfuse.start_observation(name="span") # Get ID for verification span_id = span.id @@ -843,7 +1099,7 @@ def test_create_span_and_get_observation(): sleep(2) # Use API to fetch the observation by ID - observation = get_api().observations.get(span_id) + observation = get_api().legacy.observations_v1.get(span_id) # Verify observation properties assert observation.name == "span" @@ -854,7 +1110,7 @@ def test_update_generation(): langfuse = Langfuse() # Create a generation - generation = langfuse.start_generation(name="generation") + generation = langfuse.start_observation(as_type="generation", name="generation") # Update generation with metadata generation.update(metadata={"dict": "value"}) @@ -890,7 +1146,7 @@ def test_update_span(): langfuse = Langfuse() # Create a span - span = langfuse.start_span(name="span") + span = langfuse.start_observation(name="span") # Update the span with metadata span.update(metadata={"dict": "value"}) @@ -923,7 +1179,7 @@ def test_create_span_and_generation(): langfuse = Langfuse() # Create initial span - span = langfuse.start_span(name="span") + span = langfuse.start_observation(name="span") sleep(0.1) # Get trace ID for later use trace_id = span.trace_id @@ -931,8 +1187,10 @@ def test_create_span_and_generation(): span.end() # Create generation in the same trace - generation = langfuse.start_generation( - name="generation", trace_context={"trace_id": trace_id} + generation = langfuse.start_observation( + as_type="generation", + name="generation", + trace_context={"trace_id": trace_id}, ) # End the generation generation.end() @@ -972,17 +1230,17 @@ def test_create_trace_with_id_and_generation(): trace_id = langfuse.create_trace_id() # Create a span in this trace using the trace context - with langfuse.start_as_current_span( + with langfuse.start_as_current_observation( name="parent-span", trace_context={"trace_id": trace_id} - ) as parent_span: - # Set trace name - parent_span.update_trace(name=trace_name) - - # Create a generation in the same trace - generation = parent_span.start_generation(name="generation") + ): + with propagate_attributes(trace_name=trace_name): + # Create a generation in the same trace + generation = langfuse.start_observation( + as_type="generation", name="generation" + ) - # End the generation - generation.end() + # End the generation + generation.end() # Ensure data is sent langfuse.flush() @@ -1010,7 +1268,8 @@ def test_end_generation(): api_wrapper = LangfuseAPI() # Create a generation - generation = langfuse.start_generation( + generation = langfuse.start_observation( + as_type="generation", name="query-generation", model="gpt-3.5-turbo", model_parameters={"max_tokens": "1000", "temperature": "0.9"}, @@ -1033,10 +1292,14 @@ def test_end_generation(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) + trace = api_wrapper.get_trace( + trace_id, + is_result_ready=lambda trace: any( + obs["name"] == "query-generation" for obs in trace.get("observations", []) + ), + ) # Find generation by name generations = [ @@ -1052,12 +1315,13 @@ def test_end_generation_with_data(): langfuse = Langfuse() # Create a parent span to set trace properties - with langfuse.start_as_current_span(name="parent-span") as parent_span: + with langfuse.start_as_current_observation(name="parent-span") as parent_span: # Get trace ID trace_id = parent_span.trace_id # Create generation - generation = langfuse.start_generation( + generation = langfuse.start_observation( + as_type="generation", name="query-generation", ) @@ -1127,7 +1391,8 @@ def test_end_generation_with_openai_token_format(): langfuse = Langfuse() # Create a generation - generation = langfuse.start_generation( + generation = langfuse.start_observation( + as_type="generation", name="query-generation", ) @@ -1180,7 +1445,7 @@ def test_end_span(): api_wrapper = LangfuseAPI() # Create a span - span = langfuse.start_span( + span = langfuse.start_observation( name="span", input={"key": "value"}, output={"key": "value"}, @@ -1214,7 +1479,7 @@ def test_end_span_with_data(): langfuse = Langfuse() # Create a span - span = langfuse.start_span( + span = langfuse.start_observation( name="span", input={"key": "value"}, output={"key": "value"}, @@ -1251,7 +1516,8 @@ def test_get_generations(): langfuse = Langfuse() # Create a first generation with random name - generation1 = langfuse.start_generation( + generation1 = langfuse.start_observation( + as_type="generation", name=create_uuid(), ) generation1.end() @@ -1259,7 +1525,8 @@ def test_get_generations(): # Create a second generation with specific name and content generation_name = create_uuid() - generation2 = langfuse.start_generation( + generation2 = langfuse.start_observation( + as_type="generation", name=generation_name, input="great-prompt", output="great-completion", @@ -1271,7 +1538,7 @@ def test_get_generations(): sleep(3) # Fetch generations using API - generations = get_api().observations.get_many(name=generation_name) + generations = get_api().legacy.observations_v1.get_many(name=generation_name) # Verify fetched generation matches what we created assert len(generations.data) == 1 @@ -1288,20 +1555,21 @@ def test_get_generations_by_user(): generation_name = create_uuid() # Create a trace with user ID and a generation as its child - with langfuse.start_as_current_span(name="test-user") as parent_span: - # Set user ID on the trace - parent_span.update_trace(name="test-user", user_id=user_id) - - # Create a generation within the trace - generation = parent_span.start_generation( - name=generation_name, - input="great-prompt", - output="great-completion", - ) - generation.end() + with langfuse.start_as_current_observation(name="test-user"): + with propagate_attributes(trace_name="test-user", user_id=user_id): + # Create a generation within the trace + generation = langfuse.start_observation( + as_type="generation", + name=generation_name, + input="great-prompt", + output="great-completion", + ) + generation.end() # Create another generation that doesn't have this user ID - other_gen = langfuse.start_generation(name="other-generation") + other_gen = langfuse.start_observation( + as_type="generation", name="other-generation" + ) other_gen.end() # Ensure data is sent @@ -1309,7 +1577,9 @@ def test_get_generations_by_user(): sleep(3) # Fetch generations by user ID using the API - generations = get_api().observations.get_many(user_id=user_id, type="GENERATION") + generations = get_api().legacy.observations_v1.get_many( + user_id=user_id, type="GENERATION" + ) # Verify fetched generation matches what we created assert len(generations.data) == 1 @@ -1321,7 +1591,7 @@ def test_get_generations_by_user(): def test_kwargs(): langfuse = Langfuse() - # Create kwargs dict with valid parameters for start_span + # Create kwargs dict with valid parameters for start_observation kwargs_dict = { "input": {"key": "value"}, "output": {"key": "value"}, @@ -1329,7 +1599,7 @@ def test_kwargs(): } # Create span with specific kwargs instead of using **kwargs_dict - span = langfuse.start_span( + span = langfuse.start_observation( name="span", input=kwargs_dict["input"], output=kwargs_dict["output"], @@ -1347,7 +1617,7 @@ def test_kwargs(): sleep(2) # Retrieve and verify - observation = get_api().observations.get(span_id) + observation = get_api().legacy.observations_v1.get(span_id) # Verify kwargs were properly set as attributes assert observation.start_time is not None @@ -1369,23 +1639,23 @@ def test_timezone_awareness(): langfuse = Langfuse() # Create a trace with various observation types - with langfuse.start_as_current_span(name="test") as parent_span: - # Set the trace name - parent_span.update_trace(name="test") - - # Get trace ID for verification - trace_id = parent_span.trace_id - - # Create a span - span = parent_span.start_span(name="span") - span.end() - - # Create a generation - generation = parent_span.start_generation(name="generation") - generation.end() + with langfuse.start_as_current_observation(name="test") as parent_span: + with propagate_attributes(trace_name="test"): + # Get trace ID for verification + trace_id = parent_span.trace_id + + # Create a span + span = parent_span.start_observation(name="span") + span.end() + + # Create a generation + generation = parent_span.start_observation( + as_type="generation", name="generation" + ) + generation.end() # In OTEL-based client, "events" are just spans with minimal duration - event_span = parent_span.start_span(name="event") + event_span = parent_span.start_observation(name="event") event_span.end() # Ensure data is sent @@ -1429,24 +1699,24 @@ def test_timezone_awareness_setting_timestamps(): langfuse = Langfuse() # Create a trace with different observation types - with langfuse.start_as_current_span(name="test") as parent_span: - # Set trace name - parent_span.update_trace(name="test") - - # Get trace ID for verification - trace_id = parent_span.trace_id - - # Create span - span = parent_span.start_span(name="span") - span.end() + with langfuse.start_as_current_observation(name="test") as parent_span: + with propagate_attributes(trace_name="test"): + # Get trace ID for verification + trace_id = parent_span.trace_id + + # Create span + span = parent_span.start_observation(name="span") + span.end() + + # Create generation + generation = parent_span.start_observation( + as_type="generation", name="generation" + ) + generation.end() - # Create generation - generation = parent_span.start_generation(name="generation") - generation.end() - - # Create event-like span - event_span = parent_span.start_span(name="event") - event_span.end() + # Create event-like span + event_span = parent_span.start_observation(name="event") + event_span.end() # Ensure data is sent langfuse.flush() @@ -1481,13 +1751,13 @@ def test_get_trace_by_session_id(): session_id = create_uuid() # Create a trace with a session_id - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace(name=trace_name, session_id=session_id) - # Get trace ID for verification - trace_id = span.trace_id + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes(trace_name=trace_name, session_id=session_id): + # Get trace ID for verification + trace_id = span.trace_id # Create another trace without a session_id - with langfuse.start_as_current_span(name=create_uuid()): + with langfuse.start_as_current_observation(name=create_uuid()): pass # Ensure data is sent @@ -1512,10 +1782,10 @@ def test_fetch_trace(): name = create_uuid() # Create a span and set trace properties - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace(name=name) - # Get trace ID for verification - trace_id = span.trace_id + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes(trace_name=name): + # Get trace ID for verification + trace_id = span.trace_id # Ensure data is sent langfuse.flush() @@ -1540,51 +1810,43 @@ def test_fetch_traces(): trace_ids = [] # First trace - with langfuse.start_as_current_span(name="test1") as span: - span.update_trace( - name=name, - session_id="session-1", - input={"key": "value"}, - output="output-value", - ) - trace_ids.append(span.trace_id) + with langfuse.start_as_current_observation(name="test1") as span: + with propagate_attributes(trace_name=name, session_id="session-1"): + span.set_trace_io(input={"key": "value"}, output="output-value") + trace_ids.append(span.trace_id) sleep(1) # Ensure traces have different timestamps # Second trace - with langfuse.start_as_current_span(name="test2") as span: - span.update_trace( - name=name, - session_id="session-1", - input={"key": "value"}, - output="output-value", - ) - trace_ids.append(span.trace_id) + with langfuse.start_as_current_observation(name="test2") as span: + with propagate_attributes(trace_name=name, session_id="session-1"): + span.set_trace_io(input={"key": "value"}, output="output-value") + trace_ids.append(span.trace_id) sleep(1) # Ensure traces have different timestamps # Third trace - with langfuse.start_as_current_span(name="test3") as span: - span.update_trace( - name=name, - session_id="session-1", - input={"key": "value"}, - output="output-value", - ) - trace_ids.append(span.trace_id) + with langfuse.start_as_current_observation(name="test3") as span: + with propagate_attributes(trace_name=name, session_id="session-1"): + span.set_trace_io(input={"key": "value"}, output="output-value") + trace_ids.append(span.trace_id) # Ensure data is sent langfuse.flush() - sleep(3) - # Fetch all traces with the same name - # Note: Using session_id in the query is causing a server error, - # but we keep the session_id in the trace data to ensure it's being stored correctly - all_traces = get_api().trace.list(name=name, limit=10) + expected_trace_ids = set(trace_ids) + api = get_api(retry=False) + + # Fetch all traces with the same name. + all_traces = wait_for_result( + lambda: api.trace.list(name=name, limit=10), + is_result_ready=lambda response: ( + {trace.id for trace in response.data} == expected_trace_ids + ), + ) # Verify we got all traces assert len(all_traces.data) == 3 - assert all_traces.meta.total_items == 3 # Verify trace properties for trace in all_traces.data: @@ -1593,11 +1855,19 @@ def test_fetch_traces(): assert trace.input == {"key": "value"} assert trace.output == "output-value" - # Test pagination by fetching just one trace - paginated_response = get_api().trace.list(name=name, limit=1, page=2) - assert len(paginated_response.data) == 1 - assert paginated_response.meta.total_items == 3 - assert paginated_response.meta.total_pages == 3 + # Test pagination by fetching the first three pages one at a time and + # confirming they collectively cover the created traces. + paginated_ids = set() + for page in range(1, 4): + paginated_response = wait_for_result( + lambda page=page: api.trace.list(name=name, limit=1, page=page), + is_result_ready=lambda response: ( + len(response.data) == 1 and response.data[0].id in expected_trace_ids + ), + ) + paginated_ids.add(paginated_response.data[0].id) + + assert paginated_ids == expected_trace_ids def test_get_observation(): @@ -1607,24 +1877,23 @@ def test_get_observation(): name = create_uuid() # Create a span and set trace properties - with langfuse.start_as_current_span(name="parent-span") as parent_span: - parent_span.update_trace(name=name) - - # Create a generation as child - generation = parent_span.start_generation(name=name) + with langfuse.start_as_current_observation(name="parent-span") as parent_span: + with propagate_attributes(trace_name=name): + # Create a generation as child + generation = parent_span.start_observation(as_type="generation", name=name) - # Get IDs for verification - generation_id = generation.id + # Get IDs for verification + generation_id = generation.id - # End the generation - generation.end() + # End the generation + generation.end() # Ensure data is sent langfuse.flush() sleep(2) # Fetch the observation using the API - observation = get_api().observations.get(generation_id) + observation = get_api().legacy.observations_v1.get(generation_id) # Verify observation properties assert observation.id == generation_id @@ -1639,25 +1908,30 @@ def test_get_observations(): name = create_uuid() # Create a span and set trace properties - with langfuse.start_as_current_span(name="parent-span") as parent_span: - parent_span.update_trace(name=name) - - # Create first generation - gen1 = parent_span.start_generation(name=name) - gen1_id = gen1.id - gen1.end() - - # Create second generation - gen2 = parent_span.start_generation(name=name) - gen2_id = gen2.id - gen2.end() + with langfuse.start_as_current_observation(name="parent-span"): + with propagate_attributes(trace_name=name): + # Create first generation + gen1 = langfuse.start_observation(as_type="generation", name=name) + gen1_id = gen1.id + gen1.end() + + # Create second generation + gen2 = langfuse.start_observation(as_type="generation", name=name) + gen2_id = gen2.id + gen2.end() # Ensure data is sent langfuse.flush() - sleep(2) + api = get_api(retry=False) # Fetch observations using the API - observations = get_api().observations.get_many(name=name, limit=10) + expected_generation_ids = {gen1_id, gen2_id} + observations = wait_for_result( + lambda: api.legacy.observations_v1.get_many(name=name, limit=10), + is_result_ready=lambda response: expected_generation_ids.issubset( + {obs.id for obs in response.data} + ), + ) # Verify fetched observations assert len(observations.data) == 2 @@ -1671,28 +1945,39 @@ def test_get_observations(): assert gen1_id in gen_ids assert gen2_id in gen_ids - # Test pagination - paginated_response = get_api().observations.get_many(name=name, limit=1, page=2) - assert len(paginated_response.data) == 1 - assert paginated_response.meta.total_items == 2 # Parent span + 2 generations - assert paginated_response.meta.total_pages == 2 + # Test pagination by confirming both created generations can be reached + # across separate pages. + paginated_ids = set() + for page in range(1, 3): + paginated_response = wait_for_result( + lambda page=page: api.legacy.observations_v1.get_many( + name=name, limit=1, page=page + ), + is_result_ready=lambda response: ( + len(response.data) == 1 + and response.data[0].id in expected_generation_ids + ), + ) + paginated_ids.add(paginated_response.data[0].id) + + assert paginated_ids == expected_generation_ids def test_get_trace_not_found(): # Attempt to fetch a non-existent trace using the API with pytest.raises(Exception): - get_api().trace.get(create_uuid()) + get_api(retry=False).trace.get(create_uuid()) def test_get_observation_not_found(): # Attempt to fetch a non-existent observation using the API with pytest.raises(Exception): - get_api().observations.get(create_uuid()) + get_api(retry=False).legacy.observations_v1.get(create_uuid()) def test_get_traces_empty(): # Fetch traces with a filter that should return no results - response = get_api().trace.list(name=create_uuid()) + response = get_api(retry=False).trace.list(name=create_uuid()) assert len(response.data) == 0 assert response.meta.total_items == 0 @@ -1700,7 +1985,7 @@ def test_get_traces_empty(): def test_get_observations_empty(): # Fetch observations with a filter that should return no results - response = get_api().observations.get_many(name=create_uuid()) + response = get_api(retry=False).legacy.observations_v1.get_many(name=create_uuid()) assert len(response.data) == 0 assert response.meta.total_items == 0 @@ -1717,16 +2002,19 @@ def test_get_sessions(): # Create multiple traces with different session IDs # Create first trace - with langfuse.start_as_current_span(name=name) as span1: - span1.update_trace(name=name, session_id=session1) + with langfuse.start_as_current_observation(name=name): + with propagate_attributes(trace_name=name, session_id=session1): + pass # Create second trace - with langfuse.start_as_current_span(name=name) as span2: - span2.update_trace(name=name, session_id=session2) + with langfuse.start_as_current_observation(name=name): + with propagate_attributes(trace_name=name, session_id=session2): + pass # Create third trace - with langfuse.start_as_current_span(name=name) as span3: - span3.update_trace(name=name, session_id=session3) + with langfuse.start_as_current_observation(name=name): + with propagate_attributes(trace_name=name, session_id=session3): + pass langfuse.flush() @@ -1753,21 +2041,21 @@ def test_create_trace_sampling_zero(): trace_name = create_uuid() # Create a span with trace properties - with sample_rate=0, this will not be sent to the API - with langfuse.start_as_current_span(name="test-span") as span: - span.update_trace( - name=trace_name, + with langfuse.start_as_current_observation(name="test-span") as span: + with propagate_attributes( + trace_name=trace_name, user_id="test", metadata={"key": "value"}, tags=["tag1", "tag2"], - public=True, - ) - # Get trace ID for verification - trace_id = span.trace_id - - # Add a score and a child generation - langfuse.score_current_trace(name="score", value=0.5) - generation = span.start_generation(name="generation") - generation.end() + ): + span.set_trace_as_public() + # Get trace ID for verification + trace_id = span.trace_id + + # Add a score and a child generation + langfuse.score_current_trace(name="score", value=0.5) + generation = span.start_observation(as_type="generation", name="generation") + generation.end() # Ensure data is sent, but should be dropped due to sampling langfuse.flush() @@ -1781,8 +2069,9 @@ def test_create_trace_sampling_zero(): } -def test_mask_function(): +def test_mask_function(request): LangfuseResourceManager.reset() + request.addfinalizer(LangfuseResourceManager.reset) def mask_func(data): if isinstance(data, dict): @@ -1797,22 +2086,29 @@ def mask_func(data): api_wrapper = LangfuseAPI() # Create a root span with trace properties - with langfuse.start_as_current_span(name="test-span") as root_span: - root_span.update_trace(name="test_trace", input={"sensitive": "data"}) - # Get trace ID for later use - trace_id = root_span.trace_id - # Add output to the trace - root_span.update_trace(output={"more": "sensitive"}) - - # Create a generation as child - gen = root_span.start_generation(name="test_gen", input={"prompt": "secret"}) - gen.update(output="new_confidential") - gen.end() - - # Create a span as child - sub_span = root_span.start_span(name="test_span", input={"data": "private"}) - sub_span.update(output="new_classified") - sub_span.end() + with langfuse.start_as_current_observation(name="test-span") as root_span: + with propagate_attributes(trace_name="test_trace"): + root_span.set_trace_io(input={"sensitive": "data"}) + # Get trace ID for later use + trace_id = root_span.trace_id + # Add output to the trace + root_span.set_trace_io(output={"more": "sensitive"}) + + # Create a generation as child + gen = root_span.start_observation( + as_type="generation", + name="test_gen", + input={"prompt": "secret"}, + ) + gen.update(output="new_confidential") + gen.end() + + # Create a span as child + sub_span = root_span.start_observation( + name="test_span", input={"data": "private"} + ) + sub_span.update(output="new_classified") + sub_span.end() # Ensure data is sent langfuse.flush() @@ -1838,12 +2134,13 @@ def mask_func(data): assert fetched_span["output"] == "MASKED" # Create a root span with trace properties - with langfuse.start_as_current_span(name="test-span") as root_span: - root_span.update_trace(name="test_trace", input={"should_raise": "data"}) - # Get trace ID for later use - trace_id = root_span.trace_id - # Add output to the trace - root_span.update_trace(output={"should_raise": "sensitive"}) + with langfuse.start_as_current_observation(name="test-span") as root_span: + with propagate_attributes(trace_name="test_trace"): + root_span.set_trace_io(input={"should_raise": "data"}) + # Get trace ID for later use + trace_id = root_span.trace_id + # Add output to the trace + root_span.set_trace_io(output={"should_raise": "sensitive"}) # Ensure data is sent langfuse.flush() @@ -1867,10 +2164,11 @@ def test_generate_trace_id(): trace_id = langfuse.create_trace_id() # Create a trace with the specific ID using trace_context - with langfuse.start_as_current_span( + with langfuse.start_as_current_observation( name="test-span", trace_context={"trace_id": trace_id} - ) as span: - span.update_trace(name="test_trace") + ): + with propagate_attributes(trace_name="test_trace"): + pass langfuse.flush() @@ -1878,3 +2176,158 @@ def test_generate_trace_id(): project_id = langfuse._get_project_id() trace_url = langfuse.get_trace_url(trace_id=trace_id) assert trace_url == f"http://localhost:3000/project/{project_id}/traces/{trace_id}" + + +def test_generate_trace_url_client_disabled(): + langfuse = Langfuse(tracing_enabled=False) + + with langfuse.start_as_current_observation( + name="test-span", + ): + # The trace URL should be None because the client is disabled + trace_url = langfuse.get_trace_url() + assert trace_url is None + + langfuse.flush() + + +def test_start_as_current_observation_types(): + """Test creating different observation types using start_as_current_observation.""" + langfuse = Langfuse() + + observation_types = [ + "span", + "generation", + "agent", + "tool", + "chain", + "retriever", + "evaluator", + "embedding", + "guardrail", + ] + + with langfuse.start_as_current_observation(name="parent") as parent_span: + with propagate_attributes(trace_name="observation-types-test"): + trace_id = parent_span.trace_id + + for obs_type in observation_types: + with parent_span.start_as_current_observation( + name=f"test-{obs_type}", as_type=obs_type + ): + pass + + langfuse.flush() + sleep(2) + + api = get_api() + trace = api.trace.get(trace_id) + + # Check we have all expected observation types + found_types = {obs.type for obs in trace.observations} + expected_types = {obs_type.upper() for obs_type in observation_types} | { + "SPAN" + } # includes parent span + assert expected_types.issubset(found_types), ( + f"Missing types: {expected_types - found_types}" + ) + + # Verify each specific observation exists + for obs_type in observation_types: + observations = [ + obs + for obs in trace.observations + if obs.name == f"test-{obs_type}" and obs.type == obs_type.upper() + ] + assert len(observations) == 1, f"Expected one {obs_type.upper()} observation" + + +def test_that_generation_like_properties_are_actually_created(): + """Test that generation-like observation types properly support generation properties.""" + from langfuse._client.constants import ( + ObservationTypeGenerationLike, + get_observation_types_list, + ) + + langfuse = Langfuse() + generation_like_types = get_observation_types_list(ObservationTypeGenerationLike) + + test_model = "test-model" + test_completion_start_time = datetime.now(timezone.utc) + test_model_parameters = {"temperature": "0.7", "max_tokens": "100"} + test_usage_details = {"prompt_tokens": 10, "completion_tokens": 20} + test_cost_details = {"input": 0.01, "output": 0.02, "total": 0.03} + + with langfuse.start_as_current_observation(name="parent") as parent_span: + with propagate_attributes(trace_name="generation-properties-test"): + trace_id = parent_span.trace_id + + for obs_type in generation_like_types: + with parent_span.start_as_current_observation( + name=f"test-{obs_type}", + as_type=obs_type, + model=test_model, + completion_start_time=test_completion_start_time, + model_parameters=test_model_parameters, + usage_details=test_usage_details, + cost_details=test_cost_details, + ) as obs: + # Verify the properties are accessible on the observation object + if hasattr(obs, "model"): + assert obs.model == test_model, ( + f"{obs_type} should have model property" + ) + if hasattr(obs, "completion_start_time"): + assert ( + obs.completion_start_time == test_completion_start_time + ), f"{obs_type} should have completion_start_time property" + if hasattr(obs, "model_parameters"): + assert obs.model_parameters == test_model_parameters, ( + f"{obs_type} should have model_parameters property" + ) + if hasattr(obs, "usage_details"): + assert obs.usage_details == test_usage_details, ( + f"{obs_type} should have usage_details property" + ) + if hasattr(obs, "cost_details"): + assert obs.cost_details == test_cost_details, ( + f"{obs_type} should have cost_details property" + ) + + langfuse.flush() + + api = get_api() + trace = api.trace.get(trace_id) + + # Verify that the properties are persisted in the API for generation-like types + for obs_type in generation_like_types: + observations = [ + obs + for obs in trace.observations + if obs.name == f"test-{obs_type}" and obs.type == obs_type.upper() + ] + assert len(observations) == 1, ( + f"Expected one {obs_type.upper()} observation, but found {len(observations)}" + ) + + obs = observations[0] + + assert obs.model == test_model, f"{obs_type} should have model property" + assert obs.model_parameters == test_model_parameters, ( + f"{obs_type} should have model_parameters property" + ) + + # usage_details + assert hasattr(obs, "usage_details"), f"{obs_type} should have usage_details" + assert obs.usage_details == dict(test_usage_details, total=30), ( + f"{obs_type} should persist usage_details" + ) # API adds total + + assert obs.cost_details == test_cost_details, ( + f"{obs_type} should persist cost_details" + ) + + # completion_start_time, because of time skew not asserting time + assert obs.completion_start_time is not None, ( + f"{obs_type} should persist completion_start_time property" + ) diff --git a/tests/e2e/test_datasets.py b/tests/e2e/test_datasets.py new file mode 100644 index 000000000..8d575180a --- /dev/null +++ b/tests/e2e/test_datasets.py @@ -0,0 +1,306 @@ +import time +from datetime import timedelta + +from langfuse import Langfuse +from langfuse.api import DatasetStatus +from tests.support.utils import create_uuid, wait_for_result + + +def test_create_and_get_dataset(): + langfuse = Langfuse(debug=False) + + name = "Text with spaces " + create_uuid()[:5] + langfuse.create_dataset(name=name) + dataset = langfuse.get_dataset(name) + assert dataset.name == name + + name = create_uuid() + langfuse.create_dataset( + name=name, description="This is a test dataset", metadata={"key": "value"} + ) + dataset = langfuse.get_dataset(name) + assert dataset.name == name + assert dataset.description == "This is a test dataset" + assert dataset.metadata == {"key": "value"} + + +def test_create_dataset_item(): + langfuse = Langfuse(debug=False) + name = create_uuid() + langfuse.create_dataset(name=name) + + generation = langfuse.start_observation(as_type="generation", name="test").end() + langfuse.flush() + + input = {"input": "Hello World"} + langfuse.create_dataset_item(dataset_name=name, input=input) + langfuse.create_dataset_item( + dataset_name=name, + input=input, + expected_output="Output", + metadata={"key": "value"}, + source_observation_id=generation.id, + source_trace_id=generation.trace_id, + ) + langfuse.create_dataset_item( + input="Hello", + dataset_name=name, + ) + + dataset = langfuse.get_dataset(name) + + assert len(dataset.items) == 3 + assert dataset.items[2].input == input + assert dataset.items[2].expected_output is None + assert dataset.items[2].dataset_name == name + + assert dataset.items[1].input == input + assert dataset.items[1].expected_output == "Output" + assert dataset.items[1].metadata == {"key": "value"} + assert dataset.items[1].source_observation_id == generation.id + assert dataset.items[1].source_trace_id == generation.trace_id + assert dataset.items[1].dataset_name == name + + assert dataset.items[0].input == "Hello" + assert dataset.items[0].expected_output is None + assert dataset.items[0].metadata is None + assert dataset.items[0].source_observation_id is None + assert dataset.items[0].source_trace_id is None + assert dataset.items[0].dataset_name == name + + +def test_get_all_items(): + langfuse = Langfuse(debug=False) + name = create_uuid() + langfuse.create_dataset(name=name) + + input = {"input": "Hello World"} + for _ in range(99): + langfuse.create_dataset_item(dataset_name=name, input=input) + + dataset = langfuse.get_dataset(name) + assert len(dataset.items) == 99 + + dataset_2 = langfuse.get_dataset(name, fetch_items_page_size=9) + assert len(dataset_2.items) == 99 + + dataset_3 = langfuse.get_dataset(name, fetch_items_page_size=2) + assert len(dataset_3.items) == 99 + + +def test_upsert_and_get_dataset_item(): + langfuse = Langfuse(debug=False) + name = create_uuid() + langfuse.create_dataset(name=name) + input = {"input": "Hello World"} + item = langfuse.create_dataset_item( + dataset_name=name, input=input, expected_output=input + ) + + get_item = wait_for_result( + lambda: langfuse.api.dataset_items.get(item.id), + is_result_ready=lambda dataset_item: dataset_item.id == item.id, + ) + + assert get_item.input == input + assert get_item.id == item.id + assert get_item.expected_output == input + + new_input = {"input": "Hello World 2"} + langfuse.create_dataset_item( + dataset_name=name, + input=new_input, + id=item.id, + expected_output=new_input, + ) + + get_new_item = wait_for_result( + lambda: langfuse.api.dataset_items.get(item.id), + is_result_ready=lambda dataset_item: ( + dataset_item.id == item.id + and dataset_item.input == new_input + and dataset_item.expected_output == new_input + and dataset_item.status == DatasetStatus.ACTIVE + ), + ) + + assert get_new_item.input == new_input + assert get_new_item.id == item.id + assert get_new_item.expected_output == new_input + assert get_new_item.status == DatasetStatus.ACTIVE + + langfuse.create_dataset_item( + dataset_name=name, + input=new_input, + id=item.id, + expected_output=new_input, + status=DatasetStatus.ARCHIVED, + ) + + latest_dataset = wait_for_result( + lambda: langfuse.get_dataset(name), + is_result_ready=lambda dataset: all( + dataset_item.id != item.id for dataset_item in dataset.items + ), + ) + + assert all(dataset_item.id != item.id for dataset_item in latest_dataset.items) + + archived_item = wait_for_result( + lambda: langfuse.api.dataset_items.get(item.id), + is_result_ready=lambda dataset_item: ( + dataset_item.id == item.id + and dataset_item.input == new_input + and dataset_item.expected_output == new_input + and dataset_item.status == DatasetStatus.ARCHIVED + ), + ) + assert archived_item.input == new_input + assert archived_item.id == item.id + assert archived_item.expected_output == new_input + assert archived_item.status == DatasetStatus.ARCHIVED + + +def test_run_experiment(): + """Test running an experiment on a dataset using run_experiment().""" + langfuse = Langfuse(debug=False) + + dataset_name = create_uuid() + langfuse.create_dataset(name=dataset_name) + + input_data = {"input": "Hello World"} + langfuse.create_dataset_item(dataset_name=dataset_name, input=input_data) + + dataset = langfuse.get_dataset(dataset_name) + assert len(dataset.items) == 1 + assert dataset.items[0].input == input_data + + run_name = create_uuid() + + def simple_task(*, item, **kwargs): + return f"Processed: {item.input}" + + result = dataset.run_experiment( + name=run_name, + task=simple_task, + metadata={"key": "value"}, + ) + + langfuse.flush() + time.sleep(1) # Give API time to process + + assert result is not None + assert len(result.item_results) == 1 + assert result.item_results[0].output == f"Processed: {input_data}" + + +def test_get_dataset_with_version(): + """Test that get_dataset correctly filters items by version timestamp.""" + + langfuse = Langfuse(debug=False) + + # Create dataset + name = create_uuid() + langfuse.create_dataset(name=name) + + # Create first item + item1 = langfuse.create_dataset_item(dataset_name=name, input={"version": "v1"}) + langfuse.flush() + time.sleep(3) # Ensure persistence + + # Fetch dataset to get the actual server-assigned timestamp of item1 + dataset_after_item1 = langfuse.get_dataset(name) + assert len(dataset_after_item1.items) == 1 + item1_created_at = dataset_after_item1.items[0].created_at + + # Use a timestamp 1 second after item1's actual creation time + query_timestamp = item1_created_at + timedelta(seconds=1) + time.sleep(3) # Ensure temporal separation + + # Create second item + langfuse.create_dataset_item(dataset_name=name, input={"version": "v2"}) + langfuse.flush() + time.sleep(3) # Ensure persistence + + # Fetch at the query_timestamp (should only return first item) + dataset = langfuse.get_dataset(name, version=query_timestamp) + + # Verify only first item is retrieved + assert len(dataset.items) == 1 + assert dataset.items[0].input == {"version": "v1"} + assert dataset.items[0].id == item1.id + + # Verify fetching without version returns both items (latest) + dataset_latest = langfuse.get_dataset(name) + assert len(dataset_latest.items) == 2 + + +def test_run_experiment_with_versioned_dataset(): + """Test that running an experiment on a versioned dataset works correctly.""" + import time + from datetime import timedelta + + langfuse = Langfuse(debug=False) + + # Create dataset + name = create_uuid() + langfuse.create_dataset(name=name) + + # Create first item + langfuse.create_dataset_item( + dataset_name=name, input={"question": "What is 2+2?"}, expected_output=4 + ) + langfuse.flush() + time.sleep(3) + + # Fetch dataset to get the actual server-assigned timestamp of item1 + dataset_after_item1 = langfuse.get_dataset(name) + assert len(dataset_after_item1.items) == 1 + item1_id = dataset_after_item1.items[0].id + item1_created_at = dataset_after_item1.items[0].created_at + + # Use a timestamp 1 second after item1's creation + version_timestamp = item1_created_at + timedelta(seconds=1) + time.sleep(3) + + # Update item1 after the version timestamp (this should not affect versioned query) + langfuse.create_dataset_item( + id=item1_id, + dataset_name=name, + input={"question": "What is 4+4?"}, + expected_output=8, + ) + langfuse.flush() + time.sleep(3) + + # Create second item (after version timestamp) + langfuse.create_dataset_item( + dataset_name=name, input={"question": "What is 3+3?"}, expected_output=6 + ) + langfuse.flush() + time.sleep(3) + + # Get versioned dataset (should only have first item with ORIGINAL state) + versioned_dataset = langfuse.get_dataset(name, version=version_timestamp) + assert len(versioned_dataset.items) == 1 + assert versioned_dataset.version == version_timestamp + # Verify it returns the ORIGINAL version of item1 (before the update) + assert versioned_dataset.items[0].input == {"question": "What is 2+2?"} + assert versioned_dataset.items[0].expected_output == 4 + assert versioned_dataset.items[0].id == item1_id + + # Run a simple experiment on the versioned dataset + def simple_task(*, item, **kwargs): + # Just return a static answer + return item.expected_output + + result = versioned_dataset.run_experiment( + name="Versioned Dataset Test", + description="Testing experiment with versioned dataset", + task=simple_task, + ) + + # Verify experiment ran successfully + assert result.name == "Versioned Dataset Test" + assert len(result.item_results) == 1 # Only one item in versioned dataset + assert result.item_results[0].output == 4 diff --git a/tests/e2e/test_decorators.py b/tests/e2e/test_decorators.py new file mode 100644 index 000000000..7c289980d --- /dev/null +++ b/tests/e2e/test_decorators.py @@ -0,0 +1,2096 @@ +import asyncio +import os +import sys +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor +from time import sleep +from typing import Optional + +import pytest +from langchain_core.prompts import ChatPromptTemplate +from langchain_openai import ChatOpenAI +from opentelemetry import trace + +from langfuse import Langfuse, get_client, observe, propagate_attributes +from langfuse._client.environment_variables import LANGFUSE_PUBLIC_KEY +from langfuse._client.resource_manager import LangfuseResourceManager +from langfuse.langchain import CallbackHandler +from langfuse.media import LangfuseMedia +from tests.support.utils import get_api, wait_for_trace + +mock_metadata = {"key": "metadata"} +mock_deep_metadata = {"key": "mock_deep_metadata"} +mock_session_id = "session-id-1" +mock_args = (1, 2, 3) +mock_kwargs = {"a": 1, "b": 2, "c": 3} + + +def removeMockResourceManagerInstances(): + with LangfuseResourceManager._lock: + for public_key in list(LangfuseResourceManager._instances.keys()): + if public_key != os.getenv(LANGFUSE_PUBLIC_KEY): + LangfuseResourceManager._instances.pop(public_key) + + +def _get_observation_by_name(trace_data, name): + return next( + observation + for observation in trace_data.observations + if observation.name == name + ) + + +def _is_descendant(trace_data, child_id, ancestor_id): + observations_by_id = { + observation.id: observation for observation in trace_data.observations + } + current_id = child_id + + while current_id in observations_by_id: + current = observations_by_id[current_id] + parent_id = current.parent_observation_id + if parent_id == ancestor_id: + return True + if parent_id not in observations_by_id: + return False + current_id = parent_id + + return False + + +def test_nested_observations(): + mock_name = "test_nested_observations" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation", name="level_3", capture_output=False) + def level_3_function(): + langfuse.update_current_generation(metadata=mock_metadata) + langfuse.update_current_generation( + metadata=mock_deep_metadata, + usage_details={"input": 150, "output": 50, "total": 300}, + model="gpt-3.5-turbo", + output="mock_output", + ) + langfuse.update_current_generation(version="version-1") + with propagate_attributes( + session_id=mock_session_id, trace_name=mock_name, user_id="user_id" + ): + pass + + return "level_3" + + @observe(name="level_2_manually_set") + def level_2_function(): + level_3_function() + langfuse.update_current_span(metadata=mock_metadata) + + return "level_2" + + @observe() + def level_1_function(*args, **kwargs): + level_2_function() + + return "level_1" + + result = level_1_function( + *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id + ) + langfuse.flush() + + assert result == "level_1" # Wrapped function returns correctly + + # ID setting for span or trace + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 3 + + # trace parameters if set anywhere in the call stack + assert trace_data.session_id == mock_session_id + assert trace_data.user_id == "user_id" + assert trace_data.name == mock_name + + # Check correct nesting + adjacencies = defaultdict(list) + for o in trace_data.observations: + adjacencies[o.parent_observation_id].append(o) + + assert len(adjacencies) == 3 + + level_1_observation = next( + o + for o in trace_data.observations + if o.parent_observation_id not in [o.id for o in trace_data.observations] + ) + level_2_observation = adjacencies[level_1_observation.id][0] + level_3_observation = adjacencies[level_2_observation.id][0] + + assert level_1_observation.name == "level_1_function" + assert level_1_observation.input == {"args": list(mock_args), "kwargs": mock_kwargs} + assert level_1_observation.output == "level_1" + + assert level_2_observation.name == "level_2_manually_set" + assert level_2_observation.metadata["key"] == mock_metadata["key"] + + assert level_3_observation.name == "level_3" + assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] + assert level_3_observation.type == "GENERATION" + assert level_3_observation.calculated_total_cost > 0 + assert level_3_observation.output == "mock_output" + assert level_3_observation.version == "version-1" + + +def test_nested_observations_with_non_parentheses_decorator(): + mock_name = "test_nested_observations" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation", name="level_3", capture_output=False) + def level_3_function(): + langfuse.update_current_generation(metadata=mock_metadata) + langfuse.update_current_generation( + metadata=mock_deep_metadata, + usage_details={"input": 150, "output": 50, "total": 300}, + model="gpt-3.5-turbo", + output="mock_output", + ) + langfuse.update_current_generation(version="version-1") + + with propagate_attributes( + session_id=mock_session_id, trace_name=mock_name, user_id="user_id" + ): + pass + + return "level_3" + + @observe + def level_2_function(): + level_3_function() + langfuse.update_current_span(metadata=mock_metadata) + + return "level_2" + + @observe + def level_1_function(*args, **kwargs): + level_2_function() + + return "level_1" + + result = level_1_function( + *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id + ) + langfuse.flush() + + assert result == "level_1" # Wrapped function returns correctly + + # ID setting for span or trace + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 3 + + # trace parameters if set anywhere in the call stack + assert trace_data.session_id == mock_session_id + assert trace_data.user_id == "user_id" + assert trace_data.name == mock_name + + # Check correct nesting + adjacencies = defaultdict(list) + for o in trace_data.observations: + adjacencies[o.parent_observation_id or o.trace_id].append(o) + + assert len(adjacencies) == 3 + + level_1_observation = next( + o + for o in trace_data.observations + if o.parent_observation_id not in [o.id for o in trace_data.observations] + ) + level_2_observation = adjacencies[level_1_observation.id][0] + level_3_observation = adjacencies[level_2_observation.id][0] + + assert level_1_observation.name == "level_1_function" + assert level_1_observation.input == {"args": list(mock_args), "kwargs": mock_kwargs} + assert level_1_observation.output == "level_1" + + assert level_2_observation.name == "level_2_function" + assert level_2_observation.metadata["key"] == mock_metadata["key"] + + assert level_3_observation.name == "level_3" + assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] + assert level_3_observation.type == "GENERATION" + assert level_3_observation.calculated_total_cost > 0 + assert level_3_observation.output == "mock_output" + assert level_3_observation.version == "version-1" + + +# behavior on exceptions +def test_exception_in_wrapped_function(): + mock_name = "test_exception_in_wrapped_function" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation", capture_output=False) + def level_3_function(): + langfuse.update_current_generation(metadata=mock_metadata) + langfuse.update_current_generation( + metadata=mock_deep_metadata, + usage_details={"input": 150, "output": 50, "total": 300}, + model="gpt-3.5-turbo", + ) + with propagate_attributes(session_id=mock_session_id, trace_name=mock_name): + pass + + raise ValueError("Mock exception") + + @observe() + def level_2_function(): + level_3_function() + langfuse.update_current_generation(metadata=mock_metadata) + + return "level_2" + + @observe() + def level_1_function(*args, **kwargs): + sleep(1) + level_2_function() + print("hello") + + return "level_1" + + # Check that the exception is raised + with pytest.raises(ValueError): + level_1_function(*mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id) + + langfuse.flush() + + trace_data = get_api().trace.get(mock_trace_id) + + # trace parameters if set anywhere in the call stack + assert trace_data.session_id == mock_session_id + assert trace_data.name == mock_name + + adjacencies = defaultdict(list) + for o in trace_data.observations: + adjacencies[o.parent_observation_id or o.trace_id].append(o) + + assert len(adjacencies) == 3 + + level_1_observation = next( + o + for o in trace_data.observations + if o.parent_observation_id not in [o.id for o in trace_data.observations] + ) + level_2_observation = adjacencies[level_1_observation.id][0] + level_3_observation = adjacencies[level_2_observation.id][0] + + assert level_1_observation.name == "level_1_function" + assert level_1_observation.input == {"args": list(mock_args), "kwargs": mock_kwargs} + + assert level_2_observation.name == "level_2_function" + + assert level_3_observation.name == "level_3_function" + assert level_3_observation.type == "GENERATION" + + assert level_3_observation.status_message == "Mock exception" + assert level_3_observation.level == "ERROR" + + +# behavior on concurrency +def test_concurrent_decorator_executions(): + mock_name = "test_concurrent_decorator_executions" + langfuse = get_client() + mock_trace_id_1 = langfuse.create_trace_id() + mock_trace_id_2 = langfuse.create_trace_id() + + @observe(as_type="generation", capture_output=False) + def level_3_function(): + langfuse.update_current_generation(metadata=mock_metadata) + langfuse.update_current_generation(metadata=mock_deep_metadata) + langfuse.update_current_generation( + metadata=mock_deep_metadata, + usage_details={"input": 150, "output": 50, "total": 300}, + model="gpt-3.5-turbo", + ) + with propagate_attributes(trace_name=mock_name, session_id=mock_session_id): + pass + + return "level_3" + + @observe() + def level_2_function(): + level_3_function() + langfuse.update_current_generation(metadata=mock_metadata) + + return "level_2" + + @observe(name=mock_name) + def level_1_function(*args, **kwargs): + sleep(1) + level_2_function() + + return "level_1" + + with ThreadPoolExecutor(max_workers=2) as executor: + future1 = executor.submit( + level_1_function, + *mock_args, + mock_trace_id_1, + **mock_kwargs, + langfuse_trace_id=mock_trace_id_1, + ) + future2 = executor.submit( + level_1_function, + *mock_args, + mock_trace_id_2, + **mock_kwargs, + langfuse_trace_id=mock_trace_id_2, + ) + + future1.result() + future2.result() + + langfuse.flush() + + for mock_id in [mock_trace_id_1, mock_trace_id_2]: + trace_data = get_api().trace.get(mock_id) + assert len(trace_data.observations) == 3 + + # ID setting for span or trace + assert trace_data.session_id == mock_session_id + assert trace_data.name == mock_name + + # Check correct nesting + adjacencies = defaultdict(list) + for o in trace_data.observations: + adjacencies[o.parent_observation_id].append(o) + + assert len(adjacencies) == 3 + + level_1_observation = next( + o + for o in trace_data.observations + if o.parent_observation_id not in [o.id for o in trace_data.observations] + ) + level_2_observation = adjacencies[level_1_observation.id][0] + level_3_observation = adjacencies[level_2_observation.id][0] + + assert level_1_observation.name == mock_name + assert level_1_observation.input == { + "args": list(mock_args) + [mock_id], + "kwargs": mock_kwargs, + } + assert level_1_observation.output == "level_1" + + assert level_2_observation.metadata["key"] == mock_metadata["key"] + + assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] + assert level_3_observation.type == "GENERATION" + assert level_3_observation.calculated_total_cost > 0 + + +def test_decorators_langchain(): + mock_name = "test_decorators_langchain" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe() + def langchain_operations(*args, **kwargs): + # Get langfuse callback handler for LangChain + handler = CallbackHandler() + prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}") + model = ChatOpenAI(temperature=0) + + chain = prompt | model + + return chain.invoke( + {"topic": kwargs["topic"]}, + config={ + "callbacks": [handler], + }, + ) + + @observe() + def level_3_function(*args, **kwargs): + langfuse.update_current_span(metadata=mock_metadata) + langfuse.update_current_span(metadata=mock_deep_metadata) + return langchain_operations(*args, **kwargs) + + @observe() + def level_2_function(*args, **kwargs): + langfuse.update_current_span(metadata=mock_metadata) + + return level_3_function(*args, **kwargs) + + @observe() + def level_1_function(*args, **kwargs): + return level_2_function(*args, **kwargs) + + with propagate_attributes(session_id=mock_session_id, trace_name=mock_name): + level_1_function(topic="socks", langfuse_trace_id=mock_trace_id) + + langfuse.flush() + + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + trace.session_id == mock_session_id + and trace.name == mock_name + and { + "level_1_function", + "level_2_function", + "level_3_function", + "langchain_operations", + "ChatPromptTemplate", + }.issubset({observation.name for observation in trace.observations}) + ), + ) + assert len(trace_data.observations) > 2 + + # trace parameters if set anywhere in the call stack + assert trace_data.session_id == mock_session_id + assert trace_data.name == mock_name + + level_1_observation = _get_observation_by_name(trace_data, "level_1_function") + level_2_observation = _get_observation_by_name(trace_data, "level_2_function") + level_3_observation = _get_observation_by_name(trace_data, "level_3_function") + langchain_observation = _get_observation_by_name(trace_data, "langchain_operations") + prompt_observation = _get_observation_by_name(trace_data, "ChatPromptTemplate") + + assert level_1_observation.name == "level_1_function" + assert _is_descendant(trace_data, level_2_observation.id, level_1_observation.id) + assert level_2_observation.name == "level_2_function" + assert level_2_observation.metadata["key"] == mock_metadata["key"] + assert _is_descendant(trace_data, level_3_observation.id, level_2_observation.id) + assert level_3_observation.name == "level_3_function" + assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] + assert _is_descendant(trace_data, langchain_observation.id, level_3_observation.id) + assert langchain_observation.name == "langchain_operations" + + # Check that LangChain components are captured + assert _is_descendant(trace_data, prompt_observation.id, langchain_observation.id) + + +def test_get_current_trace_url(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe() + def level_3_function(): + return langfuse.get_trace_url(trace_id=langfuse.get_current_trace_id()) + + @observe() + def level_2_function(): + return level_3_function() + + @observe() + def level_1_function(*args, **kwargs): + return level_2_function() + + result = level_1_function( + *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id + ) + langfuse.flush() + + expected_url = f"http://localhost:3000/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a/traces/{mock_trace_id}" + assert result == expected_url + + +def test_scoring_observations(): + mock_name = "test_scoring_observations" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation", capture_output=False) + def level_3_function(): + langfuse.score_current_span(name="test-observation-score", value=1) + langfuse.score_current_trace(name="another-test-trace-score", value="my_value") + + return "level_3" + + @observe() + def level_2_function(): + return level_3_function() + + @observe() + def level_1_function(*args, **kwargs): + langfuse.score_current_trace(name="test-trace-score", value=3) + with propagate_attributes(trace_name=mock_name): + return level_2_function() + + result = level_1_function( + *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id + ) + langfuse.flush() + + assert result == "level_3" # Wrapped function returns correctly + + # ID setting for span or trace + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: { + "test-observation-score", + "test-trace-score", + "another-test-trace-score", + }.issubset({score.name for score in trace.scores}), + ) + assert ( + len(trace_data.observations) == 3 + ) # Top-most function is trace, so it's not an observations + assert trace_data.name == mock_name + + # Check for correct scoring + scores_by_name = defaultdict(list) + for score in trace_data.scores: + scores_by_name[score.name].append(score) + + assert len(scores_by_name["test-trace-score"]) == 1 + assert len(scores_by_name["another-test-trace-score"]) == 1 + assert len(scores_by_name["test-observation-score"]) == 1 + + trace_scores = [ + scores_by_name["test-trace-score"][0], + scores_by_name["another-test-trace-score"][0], + ] + observation_score = scores_by_name["test-observation-score"][0] + + assert any( + [ + score.name == "another-test-trace-score" + and score.string_value == "my_value" + and score.data_type == "CATEGORICAL" + for score in trace_scores + ] + ) + assert any( + [ + score.name == "test-trace-score" + and score.value == 3 + and score.data_type == "NUMERIC" + for score in trace_scores + ] + ) + + assert observation_score.name == "test-observation-score" + assert observation_score.value == 1 + assert observation_score.data_type == "NUMERIC" + + +def test_circular_reference_handling(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + # Define a class that will contain a circular reference + class CircularRefObject: + def __init__(self): + self.reference: Optional[CircularRefObject] = None + + @observe() + def function_with_circular_arg(circular_obj, *args, **kwargs): + # This function doesn't need to do anything with circular_obj, + # the test is simply to see if it can be called without error. + return "function response" + + # Create an instance of the object and establish a circular reference + circular_obj = CircularRefObject() + circular_obj.reference = circular_obj + + # Call the decorated function, passing the circularly-referenced object + result = function_with_circular_arg(circular_obj, langfuse_trace_id=mock_trace_id) + + langfuse.flush() + + # Validate that the function executed as expected + assert result == "function response" + + trace_data = get_api().trace.get(mock_trace_id) + + assert ( + trace_data.observations[0].input["args"][0]["reference"] == "CircularRefObject" + ) + + +def test_disabled_io_capture(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + class Node: + def __init__(self, value: tuple): + self.value = value + + @observe(capture_input=False, capture_output=False) + def nested(*args, **kwargs): + langfuse.update_current_span( + input=Node(("manually set tuple", 1)), output="manually set output" + ) + return "nested response" + + @observe(capture_output=False) + def main(*args, **kwargs): + nested(*args, **kwargs) + return "function response" + + result = main("Hello, World!", name="John", langfuse_trace_id=mock_trace_id) + + langfuse.flush() + + assert result == "function response" + + trace_data = get_api().trace.get(mock_trace_id) + + # Check that disabled capture_io doesn't capture manually set input/output + assert len(trace_data.observations) == 2 + # Only one of the observations must satisfy this + found_match = False + for observation in trace_data.observations: + if ( + observation.input + and isinstance(observation.input, dict) + and "value" in observation.input + and observation.input["value"] == ["manually set tuple", 1] + and observation.output == "manually set output" + ): + found_match = True + break + assert found_match, "No observation found with expected input and output" + + +def test_decorated_class_and_instance_methods(): + mock_name = "test_decorated_class_and_instance_methods" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + class TestClass: + @classmethod + @observe(name="class-method") + def class_method(cls, *args, **kwargs): + langfuse.update_current_span() + return "class_method" + + @observe(as_type="generation", capture_output=False) + def level_3_function(self): + langfuse.update_current_generation(metadata=mock_metadata) + langfuse.update_current_generation( + metadata=mock_deep_metadata, + usage_details={"input": 150, "output": 50, "total": 300}, + model="gpt-3.5-turbo", + output="mock_output", + ) + + with propagate_attributes(session_id=mock_session_id, trace_name=mock_name): + pass + + return "level_3" + + @observe() + def level_2_function(self): + TestClass.class_method() + + self.level_3_function() + langfuse.update_current_span(metadata=mock_metadata) + + return "level_2" + + @observe() + def level_1_function(self, *args, **kwargs): + self.level_2_function() + + return "level_1" + + result = TestClass().level_1_function( + *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id + ) + + langfuse.flush() + + assert result == "level_1" # Wrapped function returns correctly + + # ID setting for span or trace + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 4 + + # trace parameters if set anywhere in the call stack + assert trace_data.session_id == mock_session_id + assert trace_data.name == mock_name + + # Check correct nesting + adjacencies = defaultdict(list) + for o in trace_data.observations: + adjacencies[o.parent_observation_id].append(o) + + assert len(adjacencies) == 3 + + level_1_observation = next( + o + for o in trace_data.observations + if o.parent_observation_id not in [o.id for o in trace_data.observations] + ) + level_2_observation = adjacencies[level_1_observation.id][0] + + # Find level_3_observation and class_method_observation in level_2's children + level_2_children = adjacencies[level_2_observation.id] + level_3_observation = next(o for o in level_2_children if o.name != "class-method") + class_method_observation = next( + o for o in level_2_children if o.name == "class-method" + ) + + assert level_1_observation.name == "level_1_function" + assert level_1_observation.input == {"args": list(mock_args), "kwargs": mock_kwargs} + assert level_1_observation.output == "level_1" + + assert level_2_observation.name == "level_2_function" + assert level_2_observation.metadata["key"] == mock_metadata["key"] + + assert class_method_observation.name == "class-method" + assert class_method_observation.output == "class_method" + + assert level_3_observation.name == "level_3_function" + assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] + assert level_3_observation.type == "GENERATION" + assert level_3_observation.calculated_total_cost > 0 + assert level_3_observation.output == "mock_output" + + +def test_generator_as_return_value(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + mock_output = "Hello, World!" + + def custom_transform_to_string(x): + return "--".join(x) + + def generator_function(): + yield "Hello" + yield ", " + yield "World!" + + @observe(transform_to_string=custom_transform_to_string) + def nested(): + return generator_function() + + @observe() + def main(**kwargs): + gen = nested() + + result = "" + for item in gen: + result += item + + return result + + result = main(langfuse_trace_id=mock_trace_id) + langfuse.flush() + + assert result == mock_output + + trace_data = get_api().trace.get(mock_trace_id) + + # Find the main and nested observations + adjacencies = defaultdict(list) + for o in trace_data.observations: + adjacencies[o.parent_observation_id].append(o) + + main_observation = next( + o + for o in trace_data.observations + if o.parent_observation_id not in [o.id for o in trace_data.observations] + ) + nested_observation = adjacencies[main_observation.id][0] + + assert main_observation.name == "main" + assert main_observation.output == mock_output + + assert nested_observation.name == "nested" + assert nested_observation.output == "Hello--, --World!" + + +@pytest.mark.asyncio +async def test_async_generator_as_return_value(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + mock_output = "Hello, async World!" + + def custom_transform_to_string(x): + return "--".join(x) + + @observe(transform_to_string=custom_transform_to_string) + async def async_generator_function(): + await asyncio.sleep(0.1) # Simulate async operation + yield "Hello" + await asyncio.sleep(0.1) + yield ", async " + await asyncio.sleep(0.1) + yield "World!" + + @observe() + async def main_async(**kwargs): + gen = async_generator_function() + + result = "" + async for item in gen: + result += item + + return result + + result = await main_async(langfuse_trace_id=mock_trace_id) + langfuse.flush() + + assert result == mock_output + + trace_data = get_api().trace.get(mock_trace_id) + + # Check correct nesting + adjacencies = defaultdict(list) + for o in trace_data.observations: + adjacencies[o.parent_observation_id].append(o) + + main_observation = next( + o + for o in trace_data.observations + if o.parent_observation_id not in [o.id for o in trace_data.observations] + ) + nested_observation = adjacencies[main_observation.id][0] + + assert main_observation.name == "main_async" + assert main_observation.output == mock_output + + assert nested_observation.name == "async_generator_function" + assert nested_observation.output == "Hello--, async --World!" + + +@pytest.mark.asyncio +async def test_async_nested_openai_chat_stream(): + from langfuse.openai import AsyncOpenAI + + mock_name = "test_async_nested_openai_chat_stream" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + mock_tags = ["tag1", "tag2"] + mock_session_id = "session-id-1" + mock_user_id = "user-id-1" + + @observe(capture_output=False) + async def level_2_function(): + gen = await AsyncOpenAI().chat.completions.create( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "1 + 1 = "}], + temperature=0, + metadata={"someKey": "someResponse"}, + stream=True, + ) + + with propagate_attributes( + session_id=mock_session_id, + user_id=mock_user_id, + tags=mock_tags, + trace_name=mock_name, + ): + async for c in gen: + print(c) + + langfuse.update_current_span(metadata=mock_metadata) + + return "level_2" + + @observe() + async def level_1_function(*args, **kwargs): + await level_2_function() + + return "level_1" + + result = await level_1_function( + *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id + ) + langfuse.flush() + + assert result == "level_1" # Wrapped function returns correctly + + # ID setting for span or trace + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + trace.session_id == mock_session_id + and trace.name == mock_name + and { + "level_1_function", + "level_2_function", + "OpenAI-generation", + }.issubset({observation.name for observation in trace.observations}) + ), + ) + assert len(trace_data.observations) >= 3 + + # trace parameters if set anywhere in the call stack + assert trace_data.session_id == mock_session_id + assert trace_data.name == mock_name + + level_1_observation = _get_observation_by_name(trace_data, "level_1_function") + level_2_observation = _get_observation_by_name(trace_data, "level_2_function") + level_3_observation = _get_observation_by_name(trace_data, "OpenAI-generation") + assert _is_descendant(trace_data, level_2_observation.id, level_1_observation.id) + assert _is_descendant(trace_data, level_3_observation.id, level_2_observation.id) + + assert level_2_observation.metadata["key"] == mock_metadata["key"] + + generation = level_3_observation + + assert generation.name == "OpenAI-generation" + assert generation.metadata["someKey"] == "someResponse" + assert generation.input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation.type == "GENERATION" + assert "gpt-3.5-turbo" in generation.model + assert generation.start_time is not None + assert generation.end_time is not None + assert generation.start_time < generation.end_time + assert generation.model_parameters == { + "temperature": 0, + "top_p": 1, + "frequency_penalty": 0, + "max_tokens": "Infinity", + "presence_penalty": 0, + } + assert generation.usage.input is not None + assert generation.usage.output is not None + assert generation.usage.total is not None + print(generation) + assert generation.output == 2 + + +def test_generator_as_function_input(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + mock_output = "Hello, World!" + + def generator_function(): + yield "Hello" + yield ", " + yield "World!" + + @observe() + def nested(gen): + result = "" + for item in gen: + result += item + + return result + + @observe() + def main(**kwargs): + gen = generator_function() + + return nested(gen) + + result = main(langfuse_trace_id=mock_trace_id) + langfuse.flush() + + assert result == mock_output + + trace_data = get_api().trace.get(mock_trace_id) + + nested_obs = next(o for o in trace_data.observations if o.name == "nested") + + assert nested_obs.input["args"][0] == "" + assert nested_obs.output == "Hello, World!" + + observation_start_time = nested_obs.start_time + observation_end_time = nested_obs.end_time + + assert observation_start_time is not None + assert observation_end_time is not None + assert observation_start_time <= observation_end_time + + +def test_nest_list_of_generator_as_function_IO(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + def generator_function(): + yield "Hello" + yield ", " + yield "World!" + + @observe() + def nested(list_of_gens): + return list_of_gens + + @observe() + def main(**kwargs): + gen = generator_function() + + return nested([(gen, gen)]) + + main(langfuse_trace_id=mock_trace_id) + langfuse.flush() + + trace_data = get_api().trace.get(mock_trace_id) + + # Find the observation with name 'nested' + nested_observation = next(o for o in trace_data.observations if o.name == "nested") + + assert [[["", ""]]] == nested_observation.input["args"] + + assert all( + ["generator" in arg for arg in nested_observation.output[0]], + ) + + observation_start_time = nested_observation.start_time + observation_end_time = nested_observation.end_time + + assert observation_start_time is not None + assert observation_end_time is not None + assert observation_start_time <= observation_end_time + + +def test_return_dict_for_output(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + mock_output = {"key": "value"} + + @observe() + def function(): + return mock_output + + result = function(langfuse_trace_id=mock_trace_id) + langfuse.flush() + + assert result == mock_output + + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: any( + observation.name == "function" and observation.output == mock_output + for observation in trace.observations + ), + ) + assert _get_observation_by_name(trace_data, "function").output == mock_output + + +def test_media(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + with open("static/bitcoin.pdf", "rb") as pdf_file: + pdf_bytes = pdf_file.read() + + media = LangfuseMedia(content_bytes=pdf_bytes, content_type="application/pdf") + + @observe() + def main(): + sleep(1) + langfuse.set_current_trace_io( + input={ + "context": { + "nested": media, + }, + }, + output={ + "context": { + "nested": media, + }, + }, + ) + # Note: Trace-level metadata with nested media objects is tested via observation metadata + langfuse.update_current_span( + metadata={ + "context": { + "nested": media, + }, + }, + ) + + main(langfuse_trace_id=mock_trace_id) + + langfuse.flush() + + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + "@@@langfuseMedia:type=application/pdf|id=" + in (trace.input or {}).get("context", {}).get("nested", "") + and "@@@langfuseMedia:type=application/pdf|id=" + in (trace.output or {}).get("context", {}).get("nested", "") + and any( + "@@@langfuseMedia:type=application/pdf|id=" + in observation.metadata.get("context", {}).get("nested", "") + for observation in trace.observations + if observation.metadata + ) + ), + ) + + assert ( + "@@@langfuseMedia:type=application/pdf|id=" + in trace_data.input["context"]["nested"] + ) + assert ( + "@@@langfuseMedia:type=application/pdf|id=" + in trace_data.output["context"]["nested"] + ) + # Check media in observation metadata + observation = trace_data.observations[0] + assert ( + "@@@langfuseMedia:type=application/pdf|id=" + in observation.metadata["context"]["nested"] + ) + parsed_reference_string = LangfuseMedia.parse_reference_string( + observation.metadata["context"]["nested"] + ) + assert parsed_reference_string["content_type"] == "application/pdf" + assert parsed_reference_string["media_id"] is not None + assert parsed_reference_string["source"] == "bytes" + + +def test_merge_metadata_and_tags(): + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe + def nested(): + with propagate_attributes(metadata={"key2": "value2"}, tags=["tag2"]): + pass + + @observe + def main(): + with propagate_attributes(metadata={"key1": "value1"}, tags=["tag1"]): + nested() + + main(langfuse_trace_id=mock_trace_id) + + langfuse.flush() + + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + trace.metadata is not None + and trace.metadata.get("key1") == "value1" + and trace.metadata.get("key2") == "value2" + and trace.tags == ["tag1", "tag2"] + ), + ) + + assert trace_data.metadata["key1"] == "value1" + assert trace_data.metadata["key2"] == "value2" + + assert trace_data.tags == ["tag1", "tag2"] + + +# Multi-project context propagation tests +def test_multiproject_context_propagation_basic(): + """Test that nested decorated functions inherit langfuse_public_key from parent in multi-project setup""" + client1 = Langfuse() # Reads from environment + Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + try: + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_context_propagation_basic" + # Use known public key from environment + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + # In multi-project setup, must specify which client to use + langfuse = get_client(public_key=env_public_key) + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation", capture_output=False) + def level_3_function(): + # This function should inherit the public key from level_1_function + # and NOT need langfuse_public_key parameter + langfuse_client = get_client() + langfuse_client.update_current_generation(metadata={"level": "3"}) + with propagate_attributes(trace_name=mock_name): + pass + return "level_3" + + @observe() + def level_2_function(): + # This function should also inherit the public key + level_3_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "2"}) + return "level_2" + + @observe() + def level_1_function(*args, **kwargs): + # Only this top-level function receives langfuse_public_key + level_2_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "1"}) + return "level_1" + + result = level_1_function( + *mock_args, + **mock_kwargs, + langfuse_trace_id=mock_trace_id, + langfuse_public_key=env_public_key, # Only provided to top-level function + ) + + # Use the correct client for flushing + client1.flush() + + assert result == "level_1" + + # Verify trace was created properly + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + trace.name == mock_name and len(trace.observations) == 3 + ), + ) + assert len(trace_data.observations) == 3 + assert trace_data.name == mock_name + finally: + removeMockResourceManagerInstances() + + +def test_multiproject_context_propagation_deep_nesting(): + client1 = Langfuse() # Reads from environment + Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + try: + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_context_propagation_deep_nesting" + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + langfuse = get_client(public_key=env_public_key) + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation") + def level_4_function(): + langfuse_client = get_client() + langfuse_client.update_current_generation(metadata={"level": "4"}) + return "level_4" + + @observe() + def level_3_function(): + result = level_4_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "3"}) + return result + + @observe() + def level_2_function(): + result = level_3_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "2"}) + return result + + @observe() + def level_1_function(*args, **kwargs): + with propagate_attributes(trace_name=mock_name): + result = level_2_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "1"}) + return result + + result = level_1_function( + langfuse_trace_id=mock_trace_id, langfuse_public_key=env_public_key + ) + client1.flush() + + assert result == "level_4" + + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + trace.name == mock_name + and len(trace.observations) == 4 + and {"1", "2", "3", "4"} + == { + str(observation.metadata.get("level")) + for observation in trace.observations + if observation.metadata + } + ), + ) + assert len(trace_data.observations) == 4 + assert trace_data.name == mock_name + + # Verify all levels were captured + levels = [ + str(obs.metadata.get("level")) + for obs in trace_data.observations + if obs.metadata + ] + assert set(levels) == {"1", "2", "3", "4"} + finally: + removeMockResourceManagerInstances() + + +def test_multiproject_context_propagation_override(): + # Initialize two separate Langfuse instances + client1 = Langfuse() # Reads from environment + client2 = Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + try: + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_context_propagation_override" + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + langfuse = get_client(public_key=env_public_key) + mock_trace_id = langfuse.create_trace_id() + + primary_public_key = env_public_key + override_public_key = "pk-test-project2" + + @observe(as_type="generation") + def level_3_function(): + # This function explicitly overrides the inherited public key + langfuse_client = get_client(public_key=override_public_key) + langfuse_client.update_current_generation( + metadata={"used_override": "true"} + ) + return "level_3" + + @observe() + def level_2_function(): + # This function should use the overridden key when calling level_3 + level_3_function(langfuse_public_key=override_public_key) + langfuse_client = get_client(public_key=primary_public_key) + langfuse_client.update_current_span(metadata={"level": "2"}) + return "level_2" + + @observe() + def level_1_function(*args, **kwargs): + with propagate_attributes(trace_name=mock_name): + level_2_function() + return "level_1" + + result = level_1_function( + langfuse_trace_id=mock_trace_id, langfuse_public_key=primary_public_key + ) + client1.flush() + client2.flush() + + assert result == "level_1" + + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + trace.name == mock_name and len(trace.observations) == 2 + ), + ) + assert len(trace_data.observations) == 2 + assert trace_data.name == mock_name + finally: + removeMockResourceManagerInstances() + + +def test_multiproject_context_propagation_no_public_key(): + # Initialize two separate Langfuse instances + client1 = Langfuse() # Reads from environment + Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_context_propagation_no_public_key" + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + langfuse = get_client(public_key=env_public_key) + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation") + def level_3_function(): + # Should use default client since no public key provided + langfuse_client = get_client() + langfuse_client.update_current_generation(metadata={"level": "3"}) + return "level_3" + + @observe() + def level_2_function(): + result = level_3_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "2"}) + return result + + @observe() + def level_1_function(*args, **kwargs): + with propagate_attributes(trace_name=mock_name): + result = level_2_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "1"}) + return result + + # No langfuse_public_key provided - should use default client + result = level_1_function(langfuse_trace_id=mock_trace_id) + client1.flush() + + assert result == "level_3" + + # Should skip tracing entirely in multi-project setup without public key + # This is expected behavior to prevent cross-project data leakage + try: + trace_data = get_api().trace.get(mock_trace_id) + # If trace is found, it should have no observations (tracing was skipped) + assert len(trace_data.observations) == 0 + except Exception: + # Trace not found is also expected - tracing was completely disabled + pass + + # Reset instances to not leak to other test suites + removeMockResourceManagerInstances() + + +@pytest.mark.asyncio +async def test_multiproject_async_context_propagation_basic(): + """Test that nested async decorated functions inherit langfuse_public_key from parent in multi-project setup""" + client1 = Langfuse() # Reads from environment + Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + try: + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_async_context_propagation_basic" + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + langfuse = get_client(public_key=env_public_key) + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation", capture_output=False) + async def async_level_3_function(): + # This function should inherit the public key from level_1_function + # and NOT need langfuse_public_key parameter + await asyncio.sleep(0.01) # Simulate async work + langfuse_client = get_client() + langfuse_client.update_current_generation( + metadata={"level": "3", "async": True} + ) + with propagate_attributes(trace_name=mock_name): + pass + return "async_level_3" + + @observe() + async def async_level_2_function(): + # This function should also inherit the public key + result = await async_level_3_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "2", "async": True}) + return result + + @observe() + async def async_level_1_function(*args, **kwargs): + # Only this top-level function receives langfuse_public_key + result = await async_level_2_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "1", "async": True}) + return result + + result = await async_level_1_function( + *mock_args, + **mock_kwargs, + langfuse_trace_id=mock_trace_id, + langfuse_public_key=env_public_key, # Only provided to top-level function + ) + + # Use the correct client for flushing + client1.flush() + + assert result == "async_level_3" + + # Verify trace was created properly + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + trace.name == mock_name + and len(trace.observations) == 3 + and all( + observation.metadata.get("async") + for observation in trace.observations + if observation.metadata + ) + ), + ) + assert len(trace_data.observations) == 3 + assert trace_data.name == mock_name + + # Verify all observations have async metadata + async_flags = [ + obs.metadata.get("async") for obs in trace_data.observations if obs.metadata + ] + assert all(async_flags) + finally: + removeMockResourceManagerInstances() + + +@pytest.mark.asyncio +async def test_multiproject_mixed_sync_async_context_propagation(): + """Test context propagation between sync and async decorated functions in multi-project setup""" + client1 = Langfuse() # Reads from environment + Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_mixed_sync_async_context_propagation" + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + langfuse = get_client(public_key=env_public_key) + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation") + def sync_level_4_function(): + # Sync function called from async should inherit context + langfuse_client = get_client() + langfuse_client.update_current_generation( + metadata={"level": "4", "type": "sync"} + ) + return "sync_level_4" + + @observe() + async def async_level_3_function(): + # Async function calls sync function + await asyncio.sleep(0.01) + result = sync_level_4_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "3", "type": "async"}) + return result + + @observe() + async def async_level_2_function(): + # Changed to async to avoid event loop issues + result = await async_level_3_function() + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"level": "2", "type": "async"}) + return result + + @observe() + async def async_level_1_function(*args, **kwargs): + # Top-level async function + with propagate_attributes(trace_name=mock_name): + result = await async_level_2_function() + langfuse_client = get_client() + langfuse_client.update_current_span( + metadata={"level": "1", "type": "async"} + ) + return result + + result = await async_level_1_function( + langfuse_trace_id=mock_trace_id, langfuse_public_key=env_public_key + ) + client1.flush() + + assert result == "sync_level_4" + + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 4 + assert trace_data.name == mock_name + + # Verify mixed sync/async execution + types = [ + obs.metadata.get("type") for obs in trace_data.observations if obs.metadata + ] + assert "sync" in types + assert "async" in types + + # Reset instances to not leak to other test suites + removeMockResourceManagerInstances() + + +@pytest.mark.asyncio +async def test_multiproject_concurrent_async_context_isolation(): + """Test that concurrent async executions don't interfere with each other's context in multi-project setup""" + client1 = Langfuse() # Reads from environment + Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_concurrent_async_context_isolation" + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + langfuse = get_client(public_key=env_public_key) + + trace_id_1 = langfuse.create_trace_id() + trace_id_2 = langfuse.create_trace_id() + + # Use the same valid public key for both tasks to avoid credential issues + # The isolation test is about trace contexts, not different projects + public_key_1 = env_public_key + public_key_2 = env_public_key + + @observe(as_type="generation") + async def async_level_3_function(task_id): + # Simulate work and ensure contexts don't leak + await asyncio.sleep(0.1) # Ensure concurrency overlap + langfuse_client = get_client() + langfuse_client.update_current_generation( + metadata={"task_id": task_id, "level": "3"} + ) + return f"async_level_3_task_{task_id}" + + @observe() + async def async_level_2_function(task_id): + result = await async_level_3_function(task_id) + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"task_id": task_id, "level": "2"}) + return result + + @observe() + async def async_level_1_function(task_id, *args, **kwargs): + with propagate_attributes(trace_name=f"{mock_name}_task_{task_id}"): + result = await async_level_2_function(task_id) + langfuse_client = get_client() + langfuse_client.update_current_span( + metadata={"task_id": task_id, "level": "1"} + ) + return result + + # Run two concurrent async tasks with the same public key but different trace contexts + task1 = async_level_1_function( + "1", langfuse_trace_id=trace_id_1, langfuse_public_key=public_key_1 + ) + task2 = async_level_1_function( + "2", langfuse_trace_id=trace_id_2, langfuse_public_key=public_key_2 + ) + + result1, result2 = await asyncio.gather(task1, task2) + + client1.flush() + + assert result1 == "async_level_3_task_1" + assert result2 == "async_level_3_task_2" + + # Verify both traces were created correctly and didn't interfere + trace_data_1 = get_api().trace.get(trace_id_1) + trace_data_2 = get_api().trace.get(trace_id_2) + + assert trace_data_1.name == f"{mock_name}_task_1" + assert trace_data_2.name == f"{mock_name}_task_2" + + # Verify that both traces have the expected number of observations (context propagation worked) + assert ( + len(trace_data_1.observations) == 3 + ) # All 3 levels should be captured for task 1 + assert ( + len(trace_data_2.observations) == 3 + ) # All 3 levels should be captured for task 2 + + # Verify traces are properly isolated (no cross-contamination) + trace_1_names = [obs.name for obs in trace_data_1.observations] + trace_2_names = [obs.name for obs in trace_data_2.observations] + assert "async_level_1_function" in trace_1_names + assert "async_level_2_function" in trace_1_names + assert "async_level_3_function" in trace_1_names + assert "async_level_1_function" in trace_2_names + assert "async_level_2_function" in trace_2_names + assert "async_level_3_function" in trace_2_names + + # Reset instances to not leak to other test suites + removeMockResourceManagerInstances() + + +@pytest.mark.asyncio +async def test_multiproject_async_generator_context_propagation(): + """Test context propagation with async generators in multi-project setup""" + client1 = Langfuse() # Reads from environment + Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_async_generator_context_propagation" + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + langfuse = get_client(public_key=env_public_key) + mock_trace_id = langfuse.create_trace_id() + + @observe(capture_output=True) + async def async_generator_function(): + # Async generator should inherit context from parent + await asyncio.sleep(0.01) + yield "Hello" + await asyncio.sleep(0.01) + yield ", " + await asyncio.sleep(0.01) + yield "Async" + await asyncio.sleep(0.01) + yield " World!" + + @observe() + async def async_consumer_function(): + with propagate_attributes(trace_name=mock_name): + result = "" + async for item in async_generator_function(): + result += item + + langfuse_client = get_client() + langfuse_client.update_current_span( + metadata={"type": "consumer", "result": result} + ) + return result + + result = await async_consumer_function( + langfuse_trace_id=mock_trace_id, langfuse_public_key=env_public_key + ) + client1.flush() + + assert result == "Hello, Async World!" + + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 2 + assert trace_data.name == mock_name + + # Verify both generator and consumer were captured by name (most reliable test) + observation_names = [obs.name for obs in trace_data.observations] + assert "async_generator_function" in observation_names + assert "async_consumer_function" in observation_names + + # Verify that context propagation worked - both functions should be in the same trace + # This confirms that the async generator inherited the public key context + assert len(trace_data.observations) == 2 + + # Reset instances to not leak to other test suites + removeMockResourceManagerInstances() + + +@pytest.mark.asyncio +async def test_multiproject_async_context_exception_handling(): + """Test that async context is properly restored even when exceptions occur in multi-project setup""" + client1 = Langfuse() # Reads from environment + Langfuse(public_key="pk-test-project2", secret_key="sk-test-project2") + + # Verify both instances are registered + assert len(LangfuseResourceManager._instances) == 2 + + mock_name = "test_multiproject_async_context_exception_handling" + env_public_key = os.environ[LANGFUSE_PUBLIC_KEY] + langfuse = get_client(public_key=env_public_key) + mock_trace_id = langfuse.create_trace_id() + + @observe(as_type="generation") + async def async_failing_function(): + # This function should inherit context but will raise an exception + await asyncio.sleep(0.01) + langfuse_client = get_client() + langfuse_client.update_current_generation(metadata={"will_fail": True}) + with propagate_attributes(trace_name=mock_name): + raise ValueError("Async function failed") + + @observe() + async def async_caller_function(): + try: + await async_failing_function() + except ValueError: + # Context should still be available here + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"caught_exception": True}) + return "exception_handled" + + @observe() + async def async_root_function(*args, **kwargs): + result = await async_caller_function() + # Context should still be available after exception + langfuse_client = get_client() + langfuse_client.update_current_span(metadata={"root": True}) + return result + + result = await async_root_function( + langfuse_trace_id=mock_trace_id, langfuse_public_key=env_public_key + ) + client1.flush() + + assert result == "exception_handled" + + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 3 + assert trace_data.name == mock_name + + # Verify exception was properly handled and context maintained + exception_obs = next(obs for obs in trace_data.observations if obs.level == "ERROR") + assert exception_obs.status_message == "Async function failed" + + caught_obs = next( + obs + for obs in trace_data.observations + if obs.metadata and obs.metadata.get("caught_exception") + ) + assert caught_obs is not None + + # Reset instances to not leak to other test suites + removeMockResourceManagerInstances() + + +def test_sync_generator_context_preservation(): + """Test that sync generators preserve context when consumed later (e.g., by streaming responses)""" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + # Global variable to capture span information + span_info = {} + + @observe(name="sync_generator") + def create_generator(): + current_span = trace.get_current_span() + span_info["generator_span_id"] = trace.format_span_id( + current_span.get_span_context().span_id + ) + + for i in range(3): + yield f"item_{i}" + + @observe(name="root") + def root_function(): + current_span = trace.get_current_span() + span_info["root_span_id"] = trace.format_span_id( + current_span.get_span_context().span_id + ) + + # Return generator without consuming it (like FastAPI StreamingResponse would) + return create_generator() + + # Simulate the scenario where generator is consumed after root function exits + generator = root_function(langfuse_trace_id=mock_trace_id) + + # Consume generator later (like FastAPI would) + items = list(generator) + + langfuse.flush() + + # Verify results + assert items == ["item_0", "item_1", "item_2"] + assert span_info["generator_span_id"] != "0000000000000000", ( + "Generator context should be preserved" + ) + assert span_info["root_span_id"] != span_info["generator_span_id"], ( + "Should have different span IDs" + ) + + # Verify trace structure + trace_data = wait_for_trace( + mock_trace_id, + is_result_ready=lambda trace: ( + len(trace.observations) >= 2 + and {"parent_root", "child_stream"}.issubset( + { + observation.name + for observation in trace.observations + if observation.name + } + ) + ), + ) + assert len(trace_data.observations) == 2 + + # Verify both observations are present + observation_names = [obs.name for obs in trace_data.observations] + assert "root" in observation_names + assert "sync_generator" in observation_names + + # Verify generator observation has output + generator_obs = next( + obs for obs in trace_data.observations if obs.name == "sync_generator" + ) + assert generator_obs.output == "item_0item_1item_2" + + +@pytest.mark.asyncio +@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher") +async def test_async_generator_context_preservation(): + """Test that async generators preserve context when consumed later (e.g., by streaming responses)""" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + # Global variable to capture span information + span_info = {} + + @observe(name="async_generator") + async def create_async_generator(): + current_span = trace.get_current_span() + span_info["generator_span_id"] = trace.format_span_id( + current_span.get_span_context().span_id + ) + + for i in range(3): + await asyncio.sleep(0.001) # Simulate async work + yield f"async_item_{i}" + + @observe(name="root") + async def root_function(): + current_span = trace.get_current_span() + span_info["root_span_id"] = trace.format_span_id( + current_span.get_span_context().span_id + ) + + # Return generator without consuming it (like FastAPI StreamingResponse would) + return create_async_generator() + + # Simulate the scenario where generator is consumed after root function exits + generator = await root_function(langfuse_trace_id=mock_trace_id) + + # Consume generator later (like FastAPI would) + items = [] + async for item in generator: + items.append(item) + + langfuse.flush() + + # Verify results + assert items == ["async_item_0", "async_item_1", "async_item_2"] + assert span_info["generator_span_id"] != "0000000000000000", ( + "Generator context should be preserved" + ) + assert span_info["root_span_id"] != span_info["generator_span_id"], ( + "Should have different span IDs" + ) + + # Verify trace structure + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 2 + + # Verify both observations are present + observation_names = [obs.name for obs in trace_data.observations if obs.name] + assert "root" in observation_names + assert "async_generator" in observation_names + + # Verify generator observation has output + generator_obs = next( + obs for obs in trace_data.observations if obs.name == "async_generator" + ) + assert generator_obs.output == "async_item_0async_item_1async_item_2" + + +@pytest.mark.asyncio +@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher") +async def test_async_generator_context_preservation_with_trace_hierarchy(): + """Test that async generators maintain proper parent-child span relationships""" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + # Global variables to capture span information + span_info = {} + + @observe(name="child_stream") + async def child_generator(): + current_span = trace.get_current_span() + span_context = current_span.get_span_context() + span_info["child_span_id"] = trace.format_span_id(span_context.span_id) + span_info["child_trace_id"] = trace.format_trace_id(span_context.trace_id) + + for i in range(2): + await asyncio.sleep(0.001) + yield f"child_{i}" + + @observe(name="parent_root") + async def parent_function(): + current_span = trace.get_current_span() + span_context = current_span.get_span_context() + span_info["parent_span_id"] = trace.format_span_id(span_context.span_id) + span_info["parent_trace_id"] = trace.format_trace_id(span_context.trace_id) + + # Create and return child generator + return child_generator() + + # Execute parent function + generator = await parent_function(langfuse_trace_id=mock_trace_id) + + # Consume generator (simulating delayed consumption) + items = [item async for item in generator] + + langfuse.flush() + + # Verify results + assert items == ["child_0", "child_1"] + + # Verify span hierarchy + assert span_info["parent_span_id"] != span_info["child_span_id"], ( + "Parent and child should have different span IDs" + ) + assert span_info["parent_trace_id"] == span_info["child_trace_id"], ( + "Parent and child should share same trace ID" + ) + assert span_info["child_span_id"] != "0000000000000000", ( + "Child context should be preserved" + ) + + # Verify trace structure + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 2 + + # Check both observations exist + observation_names = [obs.name for obs in trace_data.observations if obs.name] + assert "parent_root" in observation_names + assert "child_stream" in observation_names + + +@pytest.mark.asyncio +@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher") +async def test_async_generator_exception_handling_with_context(): + """Test that exceptions in async generators are properly handled while preserving context""" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe(name="failing_generator") + async def failing_generator(): + current_span = trace.get_current_span() + # Verify we have valid context even when exception occurs + assert ( + trace.format_span_id(current_span.get_span_context().span_id) + != "0000000000000000" + ) + + yield "first_item" + await asyncio.sleep(0.001) + raise ValueError("Generator failure test") + yield "never_reached" # This should never execute + + @observe(name="root") + async def root_function(): + return failing_generator() + + # Execute and consume generator + generator = await root_function(langfuse_trace_id=mock_trace_id) + + items = [] + with pytest.raises(ValueError, match="Generator failure test"): + async for item in generator: + items.append(item) + + langfuse.flush() + + # Verify partial results + assert items == ["first_item"] + + # Verify trace structure - should have both observations despite exception + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 2 + + # Check that the failing generator observation has ERROR level + failing_obs = next( + obs for obs in trace_data.observations if obs.name == "failing_generator" + ) + assert failing_obs.level == "ERROR" + assert "Generator failure test" in failing_obs.status_message + + +def test_sync_generator_empty_context_preservation(): + """Test that empty sync generators work correctly with context preservation""" + langfuse = get_client() + mock_trace_id = langfuse.create_trace_id() + + @observe(name="empty_generator") + def empty_generator(): + current_span = trace.get_current_span() + # Should have valid context even for empty generator + assert ( + trace.format_span_id(current_span.get_span_context().span_id) + != "0000000000000000" + ) + return + yield # Unreachable + + @observe(name="root") + def root_function(): + return empty_generator() + + generator = root_function(langfuse_trace_id=mock_trace_id) + items = list(generator) + + langfuse.flush() + + # Verify results + assert items == [] + + # Verify trace structure + trace_data = get_api().trace.get(mock_trace_id) + assert len(trace_data.observations) == 2 + + # Verify empty generator observation + empty_obs = next( + obs for obs in trace_data.observations if obs.name == "empty_generator" + ) + assert empty_obs.output is None diff --git a/tests/e2e/test_experiments.py b/tests/e2e/test_experiments.py new file mode 100644 index 000000000..80881f144 --- /dev/null +++ b/tests/e2e/test_experiments.py @@ -0,0 +1,916 @@ +"""Comprehensive tests for Langfuse experiment functionality matching JS SDK.""" + +import time +from typing import Any, Dict, List + +import pytest +from opentelemetry import trace as otel_trace_api + +from langfuse import get_client +from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse.experiment import ( + Evaluation, + ExperimentData, + ExperimentItem, + ExperimentItemResult, +) +from tests.support.utils import create_uuid, get_api, wait_for_trace + + +@pytest.fixture +def sample_dataset(): + """Sample dataset for experiments.""" + return [ + {"input": "Germany", "expected_output": "Berlin"}, + {"input": "France", "expected_output": "Paris"}, + {"input": "Spain", "expected_output": "Madrid"}, + ] + + +def mock_task(*, item: ExperimentItem, **kwargs: Dict[str, Any]): + """Mock task function that simulates processing.""" + input_val = ( + item.get("input") + if isinstance(item, dict) + else getattr(item, "input", "unknown") + ) + return f"Capital of {input_val}" + + +def simple_evaluator(*, input, output, expected_output=None, **kwargs): + """Return output length.""" + return Evaluation(name="length_check", value=len(output)) + + +def factuality_evaluator(*, input, output, expected_output=None, **kwargs): + """Mock factuality evaluator.""" + # Simple mock: check if expected output is in the output + if expected_output and expected_output.lower() in output.lower(): + return Evaluation(name="factuality", value=1.0, comment="Correct answer found") + return Evaluation(name="factuality", value=0.0, comment="Incorrect answer") + + +def run_evaluator_average_length(*, item_results: List[ExperimentItemResult], **kwargs): + """Run evaluator that calculates average output length.""" + if not item_results: + return Evaluation(name="average_length", value=0) + + avg_length = sum(len(r.output) for r in item_results) / len(item_results) + + return Evaluation(name="average_length", value=avg_length) + + +# Basic Functionality Tests +def test_run_experiment_on_local_dataset(sample_dataset): + """Test running experiment on local dataset.""" + langfuse_client = get_client() + + result = langfuse_client.run_experiment( + name="Euro capitals", + description="Country capital experiment", + data=sample_dataset, + task=mock_task, + evaluators=[simple_evaluator, factuality_evaluator], + run_evaluators=[run_evaluator_average_length], + ) + + # Validate basic result structure + assert len(result.item_results) == 3 + assert len(result.run_evaluations) == 1 + assert result.run_evaluations[0].name == "average_length" + assert result.dataset_run_id is None # No dataset_run_id for local datasets + + # Validate item results structure + for item_result in result.item_results: + assert hasattr(item_result, "output") + assert hasattr(item_result, "evaluations") + assert hasattr(item_result, "trace_id") + assert ( + item_result.dataset_run_id is None + ) # No dataset_run_id for local datasets + assert len(item_result.evaluations) == 2 # Both evaluators should run + + # Flush and wait for server processing + langfuse_client.flush() + time.sleep(2) + + # Validate traces are correctly persisted with input/output/metadata + api = get_api() + expected_inputs = ["Germany", "France", "Spain"] + expected_outputs = ["Capital of Germany", "Capital of France", "Capital of Spain"] + + for i, item_result in enumerate(result.item_results): + trace_id = item_result.trace_id + assert trace_id is not None, f"Item {i} should have a trace_id" + + # Fetch trace from API + trace = api.trace.get(trace_id) + assert trace is not None, f"Trace {trace_id} should exist" + + # Validate trace name + assert trace.name == "experiment-item-run", ( + f"Trace {trace_id} should have correct name" + ) + + # Validate trace input - should contain the experiment item + assert trace.input is not None, f"Trace {trace_id} should have input" + expected_input = expected_inputs[i] + # The input should contain the item data in some form + assert expected_input in str(trace.input), ( + f"Trace {trace_id} input should contain '{expected_input}'" + ) + + # Validate trace output - should be the task result + assert trace.output is not None, f"Trace {trace_id} should have output" + expected_output = expected_outputs[i] + assert trace.output == expected_output, ( + f"Trace {trace_id} output should be '{expected_output}', got '{trace.output}'" + ) + + # Validate trace metadata contains experiment name + assert trace.metadata is not None, f"Trace {trace_id} should have metadata" + assert "experiment_name" in trace.metadata, ( + f"Trace {trace_id} metadata should contain experiment_name" + ) + assert trace.metadata["experiment_name"] == "Euro capitals", ( + f"Trace {trace_id} metadata should have correct experiment_name" + ) + + +def test_run_experiment_flattens_large_metadata_for_server_ingestion(): + """Server ingestion handles flattened experiment metadata on non-SDK child spans.""" + langfuse_client = get_client() + external_tracer = otel_trace_api.get_tracer("ai.langfuse-python.e2e") + external_span_name = "external-experiment-metadata-child-" + create_uuid()[:8] + + experiment_metadata = { + "mode": "offline", + "job_name": "agent-eval/PR-4", + "build_url": "https://example.com/job/agent-eval-example/job/PR-4", + "agent_name": "agent-eval-example", + } + + def task_with_external_child(*, item: ExperimentItem, **kwargs: Dict[str, Any]): + with external_tracer.start_as_current_span(external_span_name) as span: + span.set_attribute("gen_ai.operation.name", "experiment-metadata-e2e") + + return "processed" + + result = langfuse_client.run_experiment( + name="Flattened Experiment Metadata " + create_uuid()[:8], + data=[{"input": "test input", "expected_output": "processed"}], + task=task_with_external_child, + metadata=experiment_metadata, + ) + + langfuse_client.flush() + + trace_id = result.item_results[0].trace_id + assert trace_id is not None + + trace = wait_for_trace( + trace_id, + is_result_ready=lambda fetched_trace: any( + observation.name == external_span_name + for observation in fetched_trace.observations + ), + ) + + assert trace.metadata is not None + for metadata_key, metadata_value in experiment_metadata.items(): + assert trace.metadata[metadata_key] == metadata_value + + external_observation = next( + observation + for observation in trace.observations + if observation.name == external_span_name + ) + external_metadata = external_observation.metadata or {} + + assert not any( + key == LangfuseOtelSpanAttributes.EXPERIMENT_METADATA + or key.startswith(f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.") + for key in external_metadata + ) + + +def test_run_experiment_on_langfuse_dataset(): + """Test running experiment on Langfuse dataset.""" + langfuse_client = get_client() + # Create dataset + dataset_name = "test-dataset-" + create_uuid() + langfuse_client.create_dataset(name=dataset_name) + + # Add items to dataset + test_items = [ + {"input": "Germany", "expected_output": "Berlin"}, + {"input": "France", "expected_output": "Paris"}, + ] + + for item in test_items: + langfuse_client.create_dataset_item( + dataset_name=dataset_name, + input=item["input"], + expected_output=item["expected_output"], + ) + + # Get dataset and run experiment + dataset = langfuse_client.get_dataset(dataset_name) + + # Use unique experiment name for proper identification + experiment_name = "Dataset Test " + create_uuid()[:8] + result = dataset.run_experiment( + name=experiment_name, + description="Test on Langfuse dataset", + task=mock_task, + evaluators=[factuality_evaluator], + run_evaluators=[run_evaluator_average_length], + ) + + # Should have dataset run ID for Langfuse datasets + assert result.dataset_run_id is not None + assert len(result.item_results) == 2 + assert all(item.dataset_run_id is not None for item in result.item_results) + + # Flush and wait for server processing + langfuse_client.flush() + time.sleep(3) + + # Verify dataset run exists via API + api = get_api() + dataset_run = api.datasets.get_run( + dataset_name=dataset_name, run_name=result.run_name + ) + + # Validate traces are correctly persisted with input/output/metadata + expected_data = {"Germany": "Capital of Germany", "France": "Capital of France"} + dataset_run_id = result.dataset_run_id + + # Create a mapping from dataset item ID to dataset item for validation + dataset_item_map = {item.id: item for item in dataset.items} + + for i, item_result in enumerate(result.item_results): + trace_id = item_result.trace_id + assert trace_id is not None, f"Item {i} should have a trace_id" + + # Fetch trace from API + trace = api.trace.get(trace_id) + assert trace is not None, f"Trace {trace_id} should exist" + + # Validate trace name + assert trace.name == "experiment-item-run", ( + f"Trace {trace_id} should have correct name" + ) + + # Validate trace input and output match expected pairs + assert trace.input is not None, f"Trace {trace_id} should have input" + trace_input_str = str(trace.input) + + # Find which expected input this trace corresponds to + matching_input = None + for expected_input in expected_data.keys(): + if expected_input in trace_input_str: + matching_input = expected_input + break + + assert matching_input is not None, ( + f"Trace {trace_id} input '{trace_input_str}' should contain one of {list(expected_data.keys())}" + ) + + # Validate trace output matches the expected output for this input + assert trace.output is not None, f"Trace {trace_id} should have output" + expected_output = expected_data[matching_input] + assert trace.output == expected_output, ( + f"Trace {trace_id} output should be '{expected_output}', got '{trace.output}'" + ) + + # Validate trace metadata contains experiment and dataset info + assert trace.metadata is not None, f"Trace {trace_id} should have metadata" + assert "experiment_name" in trace.metadata, ( + f"Trace {trace_id} metadata should contain experiment_name" + ) + assert trace.metadata["experiment_name"] == experiment_name, ( + f"Trace {trace_id} metadata should have correct experiment_name" + ) + + # Validate dataset-specific metadata fields + assert "dataset_id" in trace.metadata, ( + f"Trace {trace_id} metadata should contain dataset_id" + ) + assert trace.metadata["dataset_id"] == dataset.id, ( + f"Trace {trace_id} metadata should have correct dataset_id" + ) + + assert "dataset_item_id" in trace.metadata, ( + f"Trace {trace_id} metadata should contain dataset_item_id" + ) + # Get the dataset item ID from metadata and validate it exists + dataset_item_id = trace.metadata["dataset_item_id"] + assert dataset_item_id in dataset_item_map, ( + f"Trace {trace_id} metadata dataset_item_id should correspond to a valid dataset item" + ) + + # Validate the dataset item input matches the trace input + dataset_item = dataset_item_map[dataset_item_id] + assert dataset_item.input == matching_input, ( + f"Trace {trace_id} should correspond to dataset item with input '{matching_input}'" + ) + + assert dataset_run is not None, f"Dataset run {dataset_run_id} should exist" + assert dataset_run.name == result.run_name, "Dataset run should have correct name" + assert dataset_run.description == "Test on Langfuse dataset", ( + "Dataset run should have correct description" + ) + + # Get dataset run items to verify trace linkage + dataset_run_items = api.dataset_run_items.list( + dataset_id=dataset.id, run_name=result.run_name + ) + assert len(dataset_run_items.data) == 2, "Dataset run should have 2 items" + + # Verify each dataset run item links to the correct trace + run_item_trace_ids = { + item.trace_id for item in dataset_run_items.data if item.trace_id + } + result_trace_ids = {item.trace_id for item in result.item_results} + + assert run_item_trace_ids == result_trace_ids, ( + f"Dataset run items should link to the same traces as experiment results. " + f"Run items: {run_item_trace_ids}, Results: {result_trace_ids}" + ) + + +# Error Handling Tests +def test_evaluator_failures_handled_gracefully(): + """Test that evaluator failures don't break the experiment.""" + langfuse_client = get_client() + + def failing_evaluator(**kwargs): + raise Exception("Evaluator failed") + + def working_evaluator(**kwargs): + return Evaluation(name="working_eval", value=1.0) + + result = langfuse_client.run_experiment( + name="Error test", + data=[{"input": "test"}], + task=lambda **kwargs: "result", + evaluators=[working_evaluator, failing_evaluator], + ) + + # Should complete with only working evaluator + assert len(result.item_results) == 1 + # Only the working evaluator should have produced results + assert ( + len( + [ + eval + for eval in result.item_results[0].evaluations + if eval.name == "working_eval" + ] + ) + == 1 + ) + + langfuse_client.flush() + time.sleep(1) + + +def test_task_failures_handled_gracefully(): + """Test that task failures are handled gracefully and don't stop the experiment.""" + langfuse_client = get_client() + + def failing_task(item): + raise Exception("Task failed") + + def working_task(item): + return f"Processed: {item['input']}" + + # Test with mixed data - some will fail, some will succeed + result = langfuse_client.run_experiment( + name="Task error test", + data=[{"input": "test1"}, {"input": "test2"}], + task=failing_task, + ) + + # Should complete but with no valid results since all tasks failed + assert len(result.item_results) == 0 + + langfuse_client.flush() + time.sleep(1) + + +def test_run_evaluator_failures_handled(): + """Test that run evaluator failures don't break the experiment.""" + langfuse_client = get_client() + + def failing_run_evaluator(**kwargs): + raise Exception("Run evaluator failed") + + result = langfuse_client.run_experiment( + name="Run evaluator error test", + data=[{"input": "test"}], + task=lambda **kwargs: "result", + run_evaluators=[failing_run_evaluator], + ) + + # Should complete but run evaluations should be empty + assert len(result.item_results) == 1 + assert len(result.run_evaluations) == 0 + + langfuse_client.flush() + time.sleep(1) + + +# Edge Cases Tests +def test_empty_dataset_handling(): + """Test experiment with empty dataset.""" + langfuse_client = get_client() + + result = langfuse_client.run_experiment( + name="Empty dataset test", + data=[], + task=lambda **kwargs: "result", + run_evaluators=[run_evaluator_average_length], + ) + + assert len(result.item_results) == 0 + assert len(result.run_evaluations) == 1 # Run evaluators still execute + + langfuse_client.flush() + time.sleep(1) + + +def test_dataset_with_missing_fields(): + """Test handling dataset with missing fields.""" + langfuse_client = get_client() + + incomplete_dataset = [ + {"input": "Germany"}, # Missing expected_output + {"expected_output": "Paris"}, # Missing input + {"input": "Spain", "expected_output": "Madrid"}, # Complete + ] + + result = langfuse_client.run_experiment( + name="Incomplete data test", + data=incomplete_dataset, + task=lambda **kwargs: "result", + ) + + # Should handle missing fields gracefully + assert len(result.item_results) == 2 + for item_result in result.item_results: + assert hasattr(item_result, "trace_id") + assert hasattr(item_result, "output") + + langfuse_client.flush() + time.sleep(1) + + +def test_large_dataset_with_concurrency(): + """Test handling large dataset with concurrency control.""" + langfuse_client = get_client() + + large_dataset: ExperimentData = [ + {"input": f"Item {i}", "expected_output": f"Output {i}"} for i in range(20) + ] + + result = langfuse_client.run_experiment( + name="Large dataset test", + data=large_dataset, + task=lambda **kwargs: f"Processed {kwargs['item']}", + evaluators=[lambda **kwargs: Evaluation(name="simple_eval", value=1.0)], + max_concurrency=5, + ) + + assert len(result.item_results) == 20 + for item_result in result.item_results: + assert len(item_result.evaluations) == 1 + assert hasattr(item_result, "trace_id") + + langfuse_client.flush() + time.sleep(3) + + +# Evaluator Configuration Tests +def test_single_evaluation_return(): + """Test evaluators returning single evaluation instead of array.""" + langfuse_client = get_client() + + def single_evaluator(**kwargs): + return Evaluation(name="single_eval", value=1, comment="Single evaluation") + + result = langfuse_client.run_experiment( + name="Single evaluation test", + data=[{"input": "test"}], + task=lambda **kwargs: "result", + evaluators=[single_evaluator], + ) + + assert len(result.item_results) == 1 + assert len(result.item_results[0].evaluations) == 1 + assert result.item_results[0].evaluations[0].name == "single_eval" + + langfuse_client.flush() + time.sleep(1) + + +def test_no_evaluators(): + """Test experiment with no evaluators.""" + langfuse_client = get_client() + + result = langfuse_client.run_experiment( + name="No evaluators test", + data=[{"input": "test"}], + task=lambda **kwargs: "result", + ) + + assert len(result.item_results) == 1 + assert len(result.item_results[0].evaluations) == 0 + assert len(result.run_evaluations) == 0 + + langfuse_client.flush() + time.sleep(1) + + +def test_only_run_evaluators(): + """Test experiment with only run evaluators.""" + langfuse_client = get_client() + + def run_only_evaluator(**kwargs): + return Evaluation( + name="run_only_eval", value=10, comment="Run-level evaluation" + ) + + result = langfuse_client.run_experiment( + name="Only run evaluators test", + data=[{"input": "test"}], + task=lambda **kwargs: "result", + run_evaluators=[run_only_evaluator], + ) + + assert len(result.item_results) == 1 + assert len(result.item_results[0].evaluations) == 0 # No item evaluations + assert len(result.run_evaluations) == 1 + assert result.run_evaluations[0].name == "run_only_eval" + + langfuse_client.flush() + time.sleep(1) + + +def test_different_data_types(): + """Test evaluators returning different data types.""" + langfuse_client = get_client() + + def number_evaluator(**kwargs): + return Evaluation(name="number_eval", value=42) + + def string_evaluator(**kwargs): + return Evaluation(name="string_eval", value="excellent") + + def boolean_evaluator(**kwargs): + return Evaluation(name="boolean_eval", value=True) + + result = langfuse_client.run_experiment( + name="Different data types test", + data=[{"input": "test"}], + task=lambda **kwargs: "result", + evaluators=[number_evaluator, string_evaluator, boolean_evaluator], + ) + + evaluations = result.item_results[0].evaluations + assert len(evaluations) == 3 + + eval_by_name = {e.name: e.value for e in evaluations} + assert eval_by_name["number_eval"] == 42 + assert eval_by_name["string_eval"] == "excellent" + assert eval_by_name["boolean_eval"] is True + + langfuse_client.flush() + time.sleep(1) + + +# Data Persistence Tests +def test_scores_are_persisted(): + """Test that scores are properly persisted to the database.""" + langfuse_client = get_client() + + # Create dataset + dataset_name = "score-persistence-" + create_uuid() + langfuse_client.create_dataset(name=dataset_name) + + langfuse_client.create_dataset_item( + dataset_name=dataset_name, + input="Test input", + expected_output="Test output", + ) + + dataset = langfuse_client.get_dataset(dataset_name) + + def test_evaluator(**kwargs): + return Evaluation( + name="persistence_test", + value=0.85, + comment="Test evaluation for persistence", + ) + + def test_run_evaluator(**kwargs): + return Evaluation( + name="persistence_run_test", + value=0.9, + comment="Test run evaluation for persistence", + ) + + result = dataset.run_experiment( + name="Score persistence test", + run_name="Score persistence test", + description="Test score persistence", + task=mock_task, + evaluators=[test_evaluator], + run_evaluators=[test_run_evaluator], + ) + + assert result.dataset_run_id is not None + assert len(result.item_results) == 1 + assert len(result.run_evaluations) == 1 + + langfuse_client.flush() + time.sleep(3) + + # Verify scores are persisted via API + api = get_api() + dataset_run = api.datasets.get_run( + dataset_name=dataset_name, run_name=result.run_name + ) + + assert dataset_run.name == "Score persistence test" + + +def test_multiple_experiments_on_same_dataset(): + """Test running multiple experiments on the same dataset.""" + langfuse_client = get_client() + + # Create dataset + dataset_name = "multi-experiment-" + create_uuid() + langfuse_client.create_dataset(name=dataset_name) + + for item in [ + {"input": "Germany", "expected_output": "Berlin"}, + {"input": "France", "expected_output": "Paris"}, + ]: + langfuse_client.create_dataset_item( + dataset_name=dataset_name, + input=item["input"], + expected_output=item["expected_output"], + ) + + dataset = langfuse_client.get_dataset(dataset_name) + + # Run first experiment + result1 = dataset.run_experiment( + name="Experiment 1", + run_name="Experiment 1", + description="First experiment", + task=mock_task, + evaluators=[factuality_evaluator], + ) + + langfuse_client.flush() + time.sleep(2) + + # Run second experiment + result2 = dataset.run_experiment( + name="Experiment 2", + run_name="Experiment 2", + description="Second experiment", + task=mock_task, + evaluators=[simple_evaluator], + ) + + langfuse_client.flush() + time.sleep(2) + + # Both experiments should have different run IDs + assert result1.dataset_run_id is not None + assert result2.dataset_run_id is not None + assert result1.dataset_run_id != result2.dataset_run_id + + # Verify both runs exist in database + api = get_api() + runs = api.datasets.get_runs(dataset_name) + assert len(runs.data) >= 2 + + run_names = [run.name for run in runs.data] + assert "Experiment 1" in run_names + assert "Experiment 2" in run_names + + +# Result Formatting Tests +def test_format_experiment_results_basic(): + """Test basic result formatting functionality.""" + langfuse_client = get_client() + + result = langfuse_client.run_experiment( + name="Formatting test", + description="Test result formatting", + data=[{"input": "Hello", "expected_output": "Hi"}], + task=lambda **kwargs: f"Processed: {kwargs['item']}", + evaluators=[simple_evaluator], + run_evaluators=[run_evaluator_average_length], + ) + + # Basic validation that result structure is correct for formatting + assert len(result.item_results) == 1 + assert len(result.run_evaluations) == 1 + assert hasattr(result.item_results[0], "trace_id") + assert hasattr(result.item_results[0], "evaluations") + + langfuse_client.flush() + time.sleep(1) + + +def test_boolean_score_types(): + """Test that BOOLEAN score types are properly ingested and persisted.""" + from langfuse.api import ScoreDataType + + langfuse_client = get_client() + + def boolean_evaluator(*, input, output, expected_output=None, **kwargs): + """Boolean evaluator that checks if output contains the expected answer.""" + if not expected_output: + return Evaluation( + name="has_expected_content", + value=False, + data_type=ScoreDataType.BOOLEAN, + comment="No expected output to check", + ) + + contains_expected = expected_output.lower() in str(output).lower() + return Evaluation( + name="has_expected_content", + value=contains_expected, + data_type=ScoreDataType.BOOLEAN, + comment=f"Output {'contains' if contains_expected else 'does not contain'} expected content", + ) + + def boolean_run_evaluator(*, item_results: List[ExperimentItemResult], **kwargs): + """Run evaluator that returns boolean based on all items passing.""" + if not item_results: + return Evaluation( + name="all_items_pass", + value=False, + data_type=ScoreDataType.BOOLEAN, + comment="No items to evaluate", + ) + + # Check if all boolean evaluations are True + all_pass = True + for item_result in item_results: + for evaluation in item_result.evaluations: + if ( + evaluation.name == "has_expected_content" + and evaluation.value is False + ): + all_pass = False + break + if not all_pass: + break + + return Evaluation( + name="all_items_pass", + value=all_pass, + data_type=ScoreDataType.BOOLEAN, + comment=f"{'All' if all_pass else 'Not all'} items passed the boolean evaluation", + ) + + # Test data where some items should pass and some should fail + test_data = [ + {"input": "What is the capital of Germany?", "expected_output": "Berlin"}, + {"input": "What is the capital of France?", "expected_output": "Paris"}, + {"input": "What is the capital of Spain?", "expected_output": "Madrid"}, + ] + + # Task that returns correct answers for Germany and France, but wrong for Spain + def mock_task_with_boolean_results(*, item: ExperimentItem, **kwargs): + input_val = ( + item.get("input") + if isinstance(item, dict) + else getattr(item, "input", "unknown") + ) + input_str = str(input_val) if input_val is not None else "" + + if "Germany" in input_str: + return "The capital is Berlin" + elif "France" in input_str: + return "The capital is Paris" + else: + return "I don't know the capital" + + result = langfuse_client.run_experiment( + name="Boolean score type test", + description="Test BOOLEAN data type in scores", + data=test_data, + task=mock_task_with_boolean_results, + evaluators=[boolean_evaluator], + run_evaluators=[boolean_run_evaluator], + ) + + # Validate basic result structure + assert len(result.item_results) == 3 + assert len(result.run_evaluations) == 1 + + # Validate individual item evaluations have boolean values + expected_results = [ + True, + True, + False, + ] # Germany and France should pass, Spain should fail + for i, item_result in enumerate(result.item_results): + assert len(item_result.evaluations) == 1 + eval_result = item_result.evaluations[0] + assert eval_result.name == "has_expected_content" + assert isinstance(eval_result.value, bool) + assert eval_result.value == expected_results[i] + assert eval_result.data_type == ScoreDataType.BOOLEAN + + # Validate run evaluation is boolean and should be False (not all items passed) + run_eval = result.run_evaluations[0] + assert run_eval.name == "all_items_pass" + assert isinstance(run_eval.value, bool) + assert run_eval.value is False # Spain should fail, so not all pass + assert run_eval.data_type == ScoreDataType.BOOLEAN + + # Flush and wait for server processing + langfuse_client.flush() + time.sleep(3) + + # Verify scores are persisted via API with correct data types + for i, item_result in enumerate(result.item_results): + trace_id = item_result.trace_id + assert trace_id is not None, f"Item {i} should have a trace_id" + + # Fetch trace from API to verify score persistence + trace = wait_for_trace( + trace_id, + is_result_ready=lambda trace: len(trace.scores) > 0, + ) + assert trace is not None, f"Trace {trace_id} should exist" + + for score in trace.scores: + assert score.data_type == "BOOLEAN" + + +def test_experiment_composite_evaluator_weighted_average(): + """Test composite evaluator in experiments that computes weighted average.""" + langfuse_client = get_client() + + def accuracy_evaluator(*, input, output, **kwargs): + return Evaluation(name="accuracy", value=0.8) + + def relevance_evaluator(*, input, output, **kwargs): + return Evaluation(name="relevance", value=0.9) + + def composite_evaluator(*, input, output, expected_output, metadata, evaluations): + weights = {"accuracy": 0.6, "relevance": 0.4} + total = sum( + e.value * weights.get(e.name, 0) + for e in evaluations + if isinstance(e.value, (int, float)) + ) + + return Evaluation( + name="composite_score", + value=total, + comment=f"Weighted average of {len(evaluations)} metrics", + ) + + data = [ + {"input": "Test 1", "expected_output": "Output 1"}, + {"input": "Test 2", "expected_output": "Output 2"}, + ] + + result = langfuse_client.run_experiment( + name=f"Composite Test {create_uuid()}", + data=data, + task=mock_task, + evaluators=[accuracy_evaluator, relevance_evaluator], + composite_evaluator=composite_evaluator, + ) + + # Verify results + assert len(result.item_results) == 2 + + for item_result in result.item_results: + # Should have 3 evaluations: accuracy, relevance, and composite_score + assert len(item_result.evaluations) == 3 + eval_names = [e.name for e in item_result.evaluations] + assert "accuracy" in eval_names + assert "relevance" in eval_names + assert "composite_score" in eval_names + + # Check composite score value + composite_eval = next( + e for e in item_result.evaluations if e.name == "composite_score" + ) + expected_value = 0.8 * 0.6 + 0.9 * 0.4 # 0.84 + assert abs(composite_eval.value - expected_value) < 0.001 diff --git a/tests/e2e/test_media.py b/tests/e2e/test_media.py new file mode 100644 index 000000000..d322e1788 --- /dev/null +++ b/tests/e2e/test_media.py @@ -0,0 +1,75 @@ +import base64 +import re +from uuid import uuid4 + +from langfuse._client.client import Langfuse +from langfuse.media import LangfuseMedia +from tests.support.utils import wait_for_trace + + +def test_replace_media_reference_string_in_object(): + audio_file = "static/joke_prompt.wav" + with open(audio_file, "rb") as f: + mock_audio_bytes = f.read() + + langfuse = Langfuse() + + mock_trace_name = f"test-trace-with-audio-{uuid4()}" + base64_audio = base64.b64encode(mock_audio_bytes).decode() + + span = langfuse.start_observation( + name=mock_trace_name, + metadata={ + "context": { + "nested": LangfuseMedia( + base64_data_uri=f"data:audio/wav;base64,{base64_audio}" + ) + } + }, + ).end() + + langfuse.flush() + + fetched_trace = wait_for_trace( + span.trace_id, + is_result_ready=lambda trace: ( + bool(trace.observations) + and re.match( + r"^@@@langfuseMedia:type=audio/wav\|id=.+\|source=base64_data_uri@@@$", + trace.observations[0].metadata.get("context", {}).get("nested", ""), + ) + is not None + ), + ) + media_ref = fetched_trace.observations[0].metadata["context"]["nested"] + assert re.match( + r"^@@@langfuseMedia:type=audio/wav\|id=.+\|source=base64_data_uri@@@$", + media_ref, + ) + + resolved_obs = langfuse.resolve_media_references( + obj=fetched_trace.observations[0], resolve_with="base64_data_uri" + ) + + expected_base64 = f"data:audio/wav;base64,{base64_audio}" + assert resolved_obs["metadata"]["context"]["nested"] == expected_base64 + + span2 = langfuse.start_observation( + name=f"2-{mock_trace_name}", + metadata={"context": {"nested": resolved_obs["metadata"]["context"]["nested"]}}, + ).end() + + langfuse.flush() + + fetched_trace2 = wait_for_trace( + span2.trace_id, + is_result_ready=lambda trace: ( + bool(trace.observations) + and trace.observations[0].metadata.get("context", {}).get("nested") + == fetched_trace.observations[0].metadata["context"]["nested"] + ), + ) + assert ( + fetched_trace2.observations[0].metadata["context"]["nested"] + == fetched_trace.observations[0].metadata["context"]["nested"] + ) diff --git a/tests/e2e/test_prompt.py b/tests/e2e/test_prompt.py new file mode 100644 index 000000000..6e113cb41 --- /dev/null +++ b/tests/e2e/test_prompt.py @@ -0,0 +1,697 @@ +import pytest + +from langfuse._client.client import Langfuse +from tests.support.utils import create_uuid, get_api + + +def test_create_prompt(): + langfuse = Langfuse() + prompt_name = create_uuid() + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt="test prompt", + labels=["production"], + commit_message="initial commit", + ) + + second_prompt_client = langfuse.get_prompt(prompt_name) + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.config == second_prompt_client.config + assert prompt_client.commit_message == second_prompt_client.commit_message + assert prompt_client.config == {} + + +def test_create_prompt_with_special_chars_in_name(): + langfuse = Langfuse() + prompt_name = create_uuid() + "special chars !@#$%^&*() +" + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt="test prompt", + labels=["production"], + tags=["test"], + ) + + second_prompt_client = langfuse.get_prompt(prompt_name) + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.tags == second_prompt_client.tags + assert prompt_client.config == second_prompt_client.config + assert prompt_client.config == {} + + +def test_create_prompt_with_placeholders(): + """Test creating a prompt with placeholder messages.""" + langfuse = Langfuse() + prompt_name = create_uuid() + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt=[ + {"role": "system", "content": "System message"}, + {"type": "placeholder", "name": "context"}, + {"role": "user", "content": "User message"}, + ], + type="chat", + ) + + # Verify the full prompt structure with placeholders + assert len(prompt_client.prompt) == 3 + + # First message - system + assert prompt_client.prompt[0]["type"] == "message" + assert prompt_client.prompt[0]["role"] == "system" + assert prompt_client.prompt[0]["content"] == "System message" + # Placeholder + assert prompt_client.prompt[1]["type"] == "placeholder" + assert prompt_client.prompt[1]["name"] == "context" + # Third message - user + assert prompt_client.prompt[2]["type"] == "message" + assert prompt_client.prompt[2]["role"] == "user" + assert prompt_client.prompt[2]["content"] == "User message" + + +def test_get_prompt_with_placeholders(): + """Test retrieving a prompt with placeholders.""" + langfuse = Langfuse() + prompt_name = create_uuid() + + langfuse.create_prompt( + name=prompt_name, + prompt=[ + {"role": "system", "content": "You are {{name}}"}, + {"type": "placeholder", "name": "history"}, + {"role": "user", "content": "{{question}}"}, + ], + type="chat", + ) + + prompt_client = langfuse.get_prompt(prompt_name, type="chat", version=1) + + # Verify placeholder structure is preserved + assert len(prompt_client.prompt) == 3 + + # First message - system with variable + assert prompt_client.prompt[0]["type"] == "message" + assert prompt_client.prompt[0]["role"] == "system" + assert prompt_client.prompt[0]["content"] == "You are {{name}}" + # Placeholder + assert prompt_client.prompt[1]["type"] == "placeholder" + assert prompt_client.prompt[1]["name"] == "history" + # Third message - user with variable + assert prompt_client.prompt[2]["type"] == "message" + assert prompt_client.prompt[2]["role"] == "user" + assert prompt_client.prompt[2]["content"] == "{{question}}" + + +def test_warning_on_unresolved_placeholders(): + """Test that a warning is emitted when compiling with unresolved placeholders.""" + from unittest.mock import patch + + langfuse = Langfuse() + prompt_name = create_uuid() + + langfuse.create_prompt( + name=prompt_name, + prompt=[ + {"role": "system", "content": "You are {{name}}"}, + {"type": "placeholder", "name": "history"}, + {"role": "user", "content": "{{question}}"}, + ], + type="chat", + ) + + prompt_client = langfuse.get_prompt(prompt_name, type="chat", version=1) + + # Test that warning is emitted when compiling with unresolved placeholders + with patch("langfuse.logger.langfuse_logger.warning") as mock_warning: + # Compile without providing the 'history' placeholder + result = prompt_client.compile(name="Assistant", question="What is 2+2?") + + # Verify the warning was called with the expected message + mock_warning.assert_called_once() + warning_message = mock_warning.call_args[0][0] + assert "Placeholders ['history'] have not been resolved" in warning_message + + # Verify the result only contains the resolved messages + assert len(result) == 3 + assert result[0]["content"] == "You are Assistant" + assert result[1]["name"] == "history" + assert result[2]["content"] == "What is 2+2?" + + +def test_compiling_chat_prompt(): + langfuse = Langfuse() + prompt_name = create_uuid() + + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt=[ + { + "role": "system", + "content": "test prompt 1 with {{state}} {{target}} {{state}}", + }, + {"role": "user", "content": "test prompt 2 with {{state}}"}, + ], + labels=["production"], + type="chat", + ) + + second_prompt_client = langfuse.get_prompt(prompt_name, type="chat") + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.labels == ["production", "latest"] + + assert second_prompt_client.compile(target="world", state="great") == [ + {"role": "system", "content": "test prompt 1 with great world great"}, + {"role": "user", "content": "test prompt 2 with great"}, + ] + + +def test_compiling_prompt(): + langfuse = Langfuse() + prompt_name = "test_compiling_prompt" + + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt='Hello, {{target}}! I hope you are {{state}}. {{undefined_variable}}. And here is some JSON that should not be compiled: {{ "key": "value" }} \ + Here is a custom var for users using str.format instead of the mustache-style double curly braces: {custom_var}', + labels=["production"], + ) + + second_prompt_client = langfuse.get_prompt(prompt_name) + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.labels == ["production", "latest"] + + compiled = second_prompt_client.compile(target="world", state="great") + + assert ( + compiled + == 'Hello, world! I hope you are great. {{undefined_variable}}. And here is some JSON that should not be compiled: {{ "key": "value" }} \ + Here is a custom var for users using str.format instead of the mustache-style double curly braces: {custom_var}' + ) + + +def test_compiling_prompt_without_character_escaping(): + langfuse = Langfuse() + prompt_name = "test_compiling_prompt_without_character_escaping" + + prompt_client = langfuse.create_prompt( + name=prompt_name, prompt="Hello, {{ some_json }}", labels=["production"] + ) + + second_prompt_client = langfuse.get_prompt(prompt_name) + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.labels == ["production", "latest"] + + some_json = '{"key": "value"}' + compiled = second_prompt_client.compile(some_json=some_json) + + assert compiled == 'Hello, {"key": "value"}' + + +def test_compiling_prompt_with_content_as_variable_name(): + langfuse = Langfuse() + prompt_name = "test_compiling_prompt_with_content_as_variable_name" + + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt="Hello, {{ content }}!", + labels=["production"], + ) + + second_prompt_client = langfuse.get_prompt(prompt_name) + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.labels == ["production", "latest"] + + compiled = second_prompt_client.compile(content="Jane") + + assert compiled == "Hello, Jane!" + + +def test_create_prompt_with_null_config(): + langfuse = Langfuse(debug=False) + + langfuse.create_prompt( + name="test_null_config", + prompt="Hello, world! I hope you are great", + labels=["production"], + config=None, + ) + + prompt = langfuse.get_prompt("test_null_config") + + assert prompt.config == {} + + +def test_create_prompt_with_tags(): + langfuse = Langfuse(debug=False) + prompt_name = create_uuid() + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + tags=["tag1", "tag2"], + ) + + prompt = langfuse.get_prompt(prompt_name, version=1) + + assert prompt.tags == ["tag1", "tag2"] + + +def test_create_prompt_with_empty_tags(): + langfuse = Langfuse(debug=False) + prompt_name = create_uuid() + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + tags=[], + ) + + prompt = langfuse.get_prompt(prompt_name, version=1) + + assert prompt.tags == [] + + +def test_create_prompt_with_previous_tags(): + langfuse = Langfuse(debug=False) + prompt_name = create_uuid() + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + ) + + prompt = langfuse.get_prompt(prompt_name, version=1) + + assert prompt.tags == [] + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + tags=["tag1", "tag2"], + ) + + prompt_v2 = langfuse.get_prompt(prompt_name, version=2) + + assert prompt_v2.tags == ["tag1", "tag2"] + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + ) + + prompt_v3 = langfuse.get_prompt(prompt_name, version=3) + + assert prompt_v3.tags == ["tag1", "tag2"] + + +def test_remove_prompt_tags(): + langfuse = Langfuse(debug=False) + prompt_name = create_uuid() + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + tags=["tag1", "tag2"], + ) + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + tags=[], + ) + + prompt_v1 = langfuse.get_prompt(prompt_name, version=1) + prompt_v2 = langfuse.get_prompt(prompt_name, version=2) + + assert prompt_v1.tags == [] + assert prompt_v2.tags == [] + + +def test_update_prompt_tags(): + langfuse = Langfuse(debug=False) + prompt_name = create_uuid() + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + tags=["tag1", "tag2"], + ) + + prompt_v1 = langfuse.get_prompt(prompt_name, version=1) + + assert prompt_v1.tags == ["tag1", "tag2"] + + langfuse.create_prompt( + name=prompt_name, + prompt="Hello, world! I hope you are great", + tags=["tag3", "tag4"], + ) + + prompt_v2 = langfuse.get_prompt(prompt_name, version=2) + + assert prompt_v2.tags == ["tag3", "tag4"] + + +def test_get_prompt_by_version_or_label(): + langfuse = Langfuse() + prompt_name = create_uuid() + + for i in range(3): + langfuse.create_prompt( + name=prompt_name, + prompt="test prompt " + str(i + 1), + labels=["production"] if i == 1 else [], + ) + + default_prompt_client = langfuse.get_prompt(prompt_name) + assert default_prompt_client.version == 2 + assert default_prompt_client.prompt == "test prompt 2" + assert default_prompt_client.labels == ["production"] + + first_prompt_client = langfuse.get_prompt(prompt_name, version=1) + assert first_prompt_client.version == 1 + assert first_prompt_client.prompt == "test prompt 1" + assert first_prompt_client.labels == [] + + second_prompt_client = langfuse.get_prompt(prompt_name, version=2) + assert second_prompt_client.version == 2 + assert second_prompt_client.prompt == "test prompt 2" + assert second_prompt_client.labels == ["production"] + + third_prompt_client = langfuse.get_prompt(prompt_name, label="latest") + assert third_prompt_client.version == 3 + assert third_prompt_client.prompt == "test prompt 3" + assert third_prompt_client.labels == ["latest"] + + +def test_prompt_end_to_end(): + langfuse = Langfuse(debug=False) + + langfuse.create_prompt( + name="test", + prompt="Hello, {{target}}! I hope you are {{state}}.", + labels=["production"], + config={"temperature": 0.5}, + ) + + prompt = langfuse.get_prompt("test") + + prompt_str = prompt.compile(target="world", state="great") + assert prompt_str == "Hello, world! I hope you are great." + assert prompt.config == {"temperature": 0.5} + + generation = langfuse.start_observation( + as_type="generation", + name="mygen", + input=prompt_str, + prompt=prompt, + ).end() + + # to check that these do not error + generation.update(prompt=prompt) + + langfuse.flush() + + api = get_api() + + trace = api.trace.get(generation.trace_id) + + assert len(trace.observations) == 1 + + generation = trace.observations[0] + assert generation.prompt_id is not None + + observation = api.legacy.observations_v1.get(generation.id) + + assert observation.prompt_id is not None + + +def test_do_not_return_fallback_if_fetch_success(): + langfuse = Langfuse() + prompt_name = create_uuid() + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt="test prompt", + labels=["production"], + ) + + second_prompt_client = langfuse.get_prompt(prompt_name, fallback="fallback") + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.config == second_prompt_client.config + assert prompt_client.config == {} + + +def test_fallback_text_prompt(): + langfuse = Langfuse() + + fallback_text_prompt = "this is a fallback text prompt with {{variable}}" + + # Should throw an error if prompt not found and no fallback provided + with pytest.raises(Exception): + langfuse.get_prompt("nonexistent_prompt") + + prompt = langfuse.get_prompt("nonexistent_prompt", fallback=fallback_text_prompt) + + assert prompt.prompt == fallback_text_prompt + assert ( + prompt.compile(variable="value") == "this is a fallback text prompt with value" + ) + + +def test_fallback_chat_prompt(): + langfuse = Langfuse() + fallback_chat_prompt = [ + {"role": "system", "content": "fallback system"}, + {"role": "user", "content": "fallback user name {{name}}"}, + ] + + # Should throw an error if prompt not found and no fallback provided + with pytest.raises(Exception): + langfuse.get_prompt("nonexistent_chat_prompt", type="chat") + + prompt = langfuse.get_prompt( + "nonexistent_chat_prompt", type="chat", fallback=fallback_chat_prompt + ) + + # Check that the prompt structure contains the fallback data (allowing for internal formatting) + assert len(prompt.prompt) == len(fallback_chat_prompt) + assert all(msg["type"] == "message" for msg in prompt.prompt) + assert prompt.prompt[0]["role"] == "system" + assert prompt.prompt[0]["content"] == "fallback system" + assert prompt.prompt[1]["role"] == "user" + assert prompt.prompt[1]["content"] == "fallback user name {{name}}" + assert prompt.compile(name="Jane") == [ + {"role": "system", "content": "fallback system"}, + {"role": "user", "content": "fallback user name Jane"}, + ] + + +def test_do_not_link_observation_if_fallback(): + langfuse = Langfuse() + + fallback_text_prompt = "this is a fallback text prompt with {{variable}}" + + # Should throw an error if prompt not found and no fallback provided + with pytest.raises(Exception): + langfuse.get_prompt("nonexistent_prompt") + + prompt = langfuse.get_prompt("nonexistent_prompt", fallback=fallback_text_prompt) + + generation = langfuse.start_observation( + as_type="generation", + name="mygen", + prompt=prompt, + input="this is a test input", + ).end() + langfuse.flush() + + api = get_api() + trace = api.trace.get(generation.trace_id) + + assert len(trace.observations) == 1 + assert trace.observations[0].prompt_id is None + + +def test_variable_names_on_content_with_variable_names(): + langfuse = Langfuse() + + prompt_client = langfuse.create_prompt( + name="test_variable_names_1", + prompt="test prompt with var names {{ var1 }} {{ var2 }}", + labels=["production"], + type="text", + ) + + second_prompt_client = langfuse.get_prompt("test_variable_names_1") + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.labels == ["production", "latest"] + + var_names = second_prompt_client.variables + + assert var_names == ["var1", "var2"] + + +def test_variable_names_on_content_with_no_variable_names(): + langfuse = Langfuse() + + prompt_client = langfuse.create_prompt( + name="test_variable_names_2", + prompt="test prompt with no var names", + labels=["production"], + type="text", + ) + + second_prompt_client = langfuse.get_prompt("test_variable_names_2") + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.labels == ["production", "latest"] + + var_names = second_prompt_client.variables + + assert var_names == [] + + +def test_variable_names_on_content_with_variable_names_chat_messages(): + langfuse = Langfuse() + + prompt_client = langfuse.create_prompt( + name="test_variable_names_3", + prompt=[ + { + "role": "system", + "content": "test prompt with template vars {{ var1 }} {{ var2 }}", + }, + {"role": "user", "content": "test prompt 2 with template vars {{ var3 }}"}, + ], + labels=["production"], + type="chat", + ) + + second_prompt_client = langfuse.get_prompt("test_variable_names_3") + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.labels == ["production", "latest"] + + var_names = second_prompt_client.variables + + assert var_names == ["var1", "var2", "var3"] + + +def test_variable_names_on_content_with_no_variable_names_chat_messages(): + langfuse = Langfuse() + prompt_name = "test_variable_names_on_content_with_no_variable_names_chat_messages" + + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt=[ + {"role": "system", "content": "test prompt with no template vars"}, + {"role": "user", "content": "test prompt 2 with no template vars"}, + ], + labels=["production"], + type="chat", + ) + + second_prompt_client = langfuse.get_prompt(prompt_name) + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.labels == ["production", "latest"] + + var_names = second_prompt_client.variables + + assert var_names == [] + + +def test_update_prompt(): + langfuse = Langfuse() + prompt_name = create_uuid() + + # Create initial prompt + langfuse.create_prompt( + name=prompt_name, + prompt="test prompt", + labels=["production"], + ) + + # Update prompt labels + updated_prompt = langfuse.update_prompt( + name=prompt_name, + version=1, + new_labels=["john", "doe"], + ) + + # Fetch prompt after update (should be invalidated) + fetched_prompt = langfuse.get_prompt(prompt_name) + + # Verify the fetched prompt matches the updated values + assert fetched_prompt.name == prompt_name + assert fetched_prompt.version == 1 + print(f"Fetched prompt labels: {fetched_prompt.labels}") + print(f"Updated prompt labels: {updated_prompt.labels}") + + # production was set by the first call, latest is managed and set by Langfuse + expected_labels = sorted(["latest", "doe", "production", "john"]) + assert sorted(fetched_prompt.labels) == expected_labels + assert sorted(updated_prompt.labels) == expected_labels + + +def test_update_prompt_in_folder(): + langfuse = Langfuse() + prompt_name = f"some-folder/{create_uuid()}" + + # Create initial prompt + langfuse.create_prompt( + name=prompt_name, + prompt="test prompt", + labels=["production"], + ) + + old_prompt_obj = langfuse.get_prompt(prompt_name) + + updated_prompt = langfuse.update_prompt( + name=old_prompt_obj.name, + version=old_prompt_obj.version, + new_labels=["john", "doe"], + ) + + # Fetch prompt after update (should be invalidated) + fetched_prompt = langfuse.get_prompt(prompt_name) + + # Verify the fetched prompt matches the updated values + assert fetched_prompt.name == prompt_name + assert fetched_prompt.version == 1 + print(f"Fetched prompt labels: {fetched_prompt.labels}") + print(f"Updated prompt labels: {updated_prompt.labels}") + + # production was set by the first call, latest is managed and set by Langfuse + expected_labels = sorted(["latest", "doe", "production", "john"]) + assert sorted(fetched_prompt.labels) == expected_labels + assert sorted(updated_prompt.labels) == expected_labels diff --git a/tests/live_provider/__init__.py b/tests/live_provider/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/live_provider/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_langchain.py b/tests/live_provider/test_langchain.py similarity index 58% rename from tests/test_langchain.py rename to tests/live_provider/test_langchain.py index 5bf764999..c1b13222e 100644 --- a/tests/test_langchain.py +++ b/tests/live_provider/test_langchain.py @@ -1,95 +1,24 @@ -import os import random import string import time from time import sleep -from typing import Any, Dict, List, Literal, Mapping, Optional +from typing import Any, Dict, Literal import pytest -from langchain.chains import ( - ConversationalRetrievalChain, - ConversationChain, - LLMChain, - RetrievalQA, - SimpleSequentialChain, -) -from langchain.chains.openai_functions import create_openai_fn_chain -from langchain.memory import ConversationBufferMemory -from langchain.prompts import ChatPromptTemplate, PromptTemplate -from langchain.schema import HumanMessage, SystemMessage -from langchain.text_splitter import CharacterTextSplitter -from langchain_community.document_loaders import TextLoader -from langchain_community.embeddings import OpenAIEmbeddings -from langchain_community.vectorstores import Chroma -from langchain_core.callbacks.manager import CallbackManagerForLLMRun -from langchain_core.language_models.llms import LLM +from langchain.messages import HumanMessage, SystemMessage from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate, PromptTemplate from langchain_core.runnables.base import RunnableLambda from langchain_core.tools import StructuredTool, tool from langchain_openai import ChatOpenAI, OpenAI from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import END, START, MessagesState, StateGraph from langgraph.prebuilt import ToolNode -from pydantic.v1 import BaseModel, Field +from pydantic import BaseModel, Field from langfuse._client.client import Langfuse from langfuse.langchain import CallbackHandler -from langfuse.langchain.CallbackHandler import LANGSMITH_TAG_HIDDEN -from tests.api_wrapper import LangfuseAPI -from tests.utils import create_uuid, encode_file_to_base64, get_api - - -def test_callback_generated_from_trace_chain(): - langfuse = Langfuse() - - with langfuse.start_as_current_span(name="parent") as span: - trace_id = span.trace_id - handler = CallbackHandler() - - llm = OpenAI(openai_api_key=os.environ.get("OPENAI_API_KEY")) - template = """You are a playwright. Given the title of play, it is your job to write a synopsis for that title. - Title: {title} - Playwright: This is a synopsis for the above play:""" - - prompt_template = PromptTemplate(input_variables=["title"], template=template) - synopsis_chain = LLMChain(llm=llm, prompt=prompt_template) - - synopsis_chain.run("Tragedy at sunset on the beach", callbacks=[handler]) - - langfuse.flush() - - trace = get_api().trace.get(trace_id) - - assert trace.input is None - assert trace.output is None - - assert len(trace.observations) == 3 - - langchain_span = list( - filter( - lambda o: o.type == "SPAN" and o.name == "LLMChain", - trace.observations, - ) - )[0] - - assert langchain_span.input is not None - assert langchain_span.output is not None - - langchain_generation_span = list( - filter( - lambda o: o.type == "GENERATION" and o.name == "OpenAI", - trace.observations, - ) - )[0] - - assert langchain_generation_span.parent_observation_id == langchain_span.id - assert langchain_generation_span.usage_details["input"] > 0 - assert langchain_generation_span.usage_details["output"] > 0 - assert langchain_generation_span.usage_details["total"] > 0 - assert langchain_generation_span.input is not None - assert langchain_generation_span.input != "" - assert langchain_generation_span.output is not None - assert langchain_generation_span.output != "" +from tests.support.utils import create_uuid, encode_file_to_base64, get_api def test_callback_generated_from_trace_chat(): @@ -97,7 +26,7 @@ def test_callback_generated_from_trace_chat(): trace_id = create_uuid() - with langfuse.start_as_current_span(name="parent") as span: + with langfuse.start_as_current_observation(name="parent") as span: trace_id = span.trace_id handler = CallbackHandler() chat = ChatOpenAI(temperature=0) @@ -111,7 +40,7 @@ def test_callback_generated_from_trace_chat(): ), ] - chat(messages, callbacks=[handler]) + chat.invoke(messages, config={"callbacks": [handler]}) langfuse.flush() @@ -122,14 +51,16 @@ def test_callback_generated_from_trace_chat(): assert trace.id == trace_id - assert len(trace.observations) == 2 + assert len(trace.observations) >= 2 + assert any(observation.name == "parent" for observation in trace.observations) - langchain_generation_span = list( - filter( - lambda o: o.type == "GENERATION" and o.name == "ChatOpenAI", - trace.observations, - ) - )[0] + generation_observations = [ + observation + for observation in trace.observations + if observation.type == "GENERATION" and observation.name == "ChatOpenAI" + ] + assert len(generation_observations) == 1 + langchain_generation_span = generation_observations[0] assert langchain_generation_span.usage_details["input"] > 0 assert langchain_generation_span.usage_details["output"] > 0 @@ -138,12 +69,13 @@ def test_callback_generated_from_trace_chat(): assert langchain_generation_span.input != "" assert langchain_generation_span.output is not None assert langchain_generation_span.output != "" + assert langchain_generation_span.metadata["is_langchain_root"] is True def test_callback_generated_from_lcel_chain(): langfuse = Langfuse() - with langfuse.start_as_current_span(name="parent") as span: + with langfuse.start_as_current_observation(name="parent") as span: trace_id = span.trace_id handler = CallbackHandler() prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}") @@ -168,12 +100,18 @@ def test_callback_generated_from_lcel_chain(): assert len(trace.observations) > 0 - langchain_generation_span = list( - filter( - lambda o: o.type == "GENERATION" and o.name == "ChatOpenAI", - trace.observations, - ) - )[0] + generation_observations = [ + observation + for observation in trace.observations + if observation.type == "GENERATION" + ] + assert len(generation_observations) > 0 + langchain_generation_span = generation_observations[0] + langchain_root_spans = [ + observation + for observation in trace.observations + if observation.metadata and observation.metadata.get("is_langchain_root") + ] assert langchain_generation_span.usage_details["input"] > 1 assert langchain_generation_span.usage_details["output"] > 0 @@ -182,8 +120,12 @@ def test_callback_generated_from_lcel_chain(): assert langchain_generation_span.input != "" assert langchain_generation_span.output is not None assert langchain_generation_span.output != "" + assert len(langchain_root_spans) == 1 + assert langchain_root_spans[0].type == "CHAIN" + assert langchain_root_spans[0].metadata["is_langchain_root"] is True +@pytest.mark.skip(reason="Flaky") def test_basic_chat_openai(): # Create a unique name for this test test_name = f"Test Basic Chat {create_uuid()}" @@ -226,179 +168,10 @@ def test_basic_chat_openai(): assert generation.output is not None -def test_callback_retriever(): - langfuse = Langfuse() - - with langfuse.start_as_current_span(name="retriever_test") as span: - trace_id = span.trace_id - handler = CallbackHandler() - - loader = TextLoader("./static/state_of_the_union.txt", encoding="utf8") - llm = OpenAI() - - documents = loader.load() - text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) - texts = text_splitter.split_documents(documents) - - embeddings = OpenAIEmbeddings() - docsearch = Chroma.from_documents(texts, embeddings) - - query = "What did the president say about Ketanji Brown Jackson" - - chain = RetrievalQA.from_chain_type( - llm, - retriever=docsearch.as_retriever(), - ) - - chain.run(query, callbacks=[handler]) - - langfuse.flush() - - trace = get_api().trace.get(trace_id) - - assert len(trace.observations) == 6 - for observation in trace.observations: - if observation.type == "GENERATION": - assert observation.usage_details["input"] > 0 - assert observation.usage_details["output"] > 0 - assert observation.usage_details["total"] > 0 - assert observation.input is not None - assert observation.input != "" - assert observation.output is not None - assert observation.output != "" - - -def test_callback_retriever_with_sources(): - langfuse = Langfuse() - - with langfuse.start_as_current_span(name="retriever_with_sources_test") as span: - trace_id = span.trace_id - handler = CallbackHandler() - - loader = TextLoader("./static/state_of_the_union.txt", encoding="utf8") - llm = OpenAI() - - documents = loader.load() - text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) - texts = text_splitter.split_documents(documents) - - embeddings = OpenAIEmbeddings() - docsearch = Chroma.from_documents(texts, embeddings) - - query = "What did the president say about Ketanji Brown Jackson" - - chain = RetrievalQA.from_chain_type( - llm, retriever=docsearch.as_retriever(), return_source_documents=True - ) - - chain(query, callbacks=[handler]) - - langfuse.flush() - - trace = get_api().trace.get(trace_id) - - assert len(trace.observations) == 6 - for observation in trace.observations: - if observation.type == "GENERATION": - assert observation.usage_details["input"] > 0 - assert observation.usage_details["output"] > 0 - assert observation.usage_details["total"] > 0 - assert observation.input is not None - assert observation.input != "" - assert observation.output is not None - assert observation.output != "" - - -def test_callback_retriever_conversational_with_memory(): - langfuse = Langfuse() - - with langfuse.start_as_current_span( - name="retriever_conversational_with_memory_test" - ) as span: - trace_id = span.trace_id - handler = CallbackHandler() - - llm = OpenAI(openai_api_key=os.environ.get("OPENAI_API_KEY")) - conversation = ConversationChain( - llm=llm, - verbose=True, - memory=ConversationBufferMemory(), - callbacks=[handler], - ) - conversation.predict(input="Hi there!", callbacks=[handler]) - - handler.client.flush() - - trace = get_api().trace.get(trace_id) - - # Add 1 to account for the wrapping span - assert len(trace.observations) == 3 - - generations = list(filter(lambda x: x.type == "GENERATION", trace.observations)) - assert len(generations) == 1 - - for generation in generations: - assert generation.input is not None - assert generation.output is not None - assert generation.input != "" - assert generation.output != "" - assert generation.usage_details["total"] is not None - assert generation.usage_details["input"] is not None - assert generation.usage_details["output"] is not None - - -def test_callback_retriever_conversational(): - langfuse = Langfuse() - - with langfuse.start_as_current_span(name="retriever_conversational_test") as span: - trace_id = span.trace_id - api_wrapper = LangfuseAPI() - handler = CallbackHandler() - - loader = TextLoader("./static/state_of_the_union.txt", encoding="utf8") - - documents = loader.load() - text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) - texts = text_splitter.split_documents(documents) - - embeddings = OpenAIEmbeddings(openai_api_key=os.environ.get("OPENAI_API_KEY")) - docsearch = Chroma.from_documents(texts, embeddings) - - query = "What did the president say about Ketanji Brown Jackson" - - chain = ConversationalRetrievalChain.from_llm( - ChatOpenAI( - openai_api_key=os.environ.get("OPENAI_API_KEY"), - temperature=0.5, - model="gpt-3.5-turbo-16k", - ), - docsearch.as_retriever(search_kwargs={"k": 6}), - return_source_documents=True, - ) - - chain({"question": query, "chat_history": []}, callbacks=[handler]) - - handler.client.flush() - - trace = api_wrapper.get_trace(trace_id) - - # Add 1 to account for the wrapping span - assert len(trace["observations"]) == 6 - for observation in trace["observations"]: - if observation["type"] == "GENERATION": - assert observation["promptTokens"] > 0 - assert observation["completionTokens"] > 0 - assert observation["totalTokens"] > 0 - assert observation["input"] is not None - assert observation["input"] != "" - assert observation["output"] is not None - assert observation["output"] != "" - - def test_callback_simple_openai(): langfuse = Langfuse() - with langfuse.start_as_current_span(name="simple_openai_test") as span: + with langfuse.start_as_current_observation(name="simple_openai_test") as span: trace_id = span.trace_id # Create a unique name for this test @@ -415,7 +188,7 @@ def test_callback_simple_openai(): llm.invoke(text, config={"callbacks": [handler], "run_name": test_name}) # Ensure data is flushed to API - handler.client.flush() + handler._langfuse_client.flush() sleep(2) # Retrieve trace @@ -438,7 +211,9 @@ def test_callback_simple_openai(): def test_callback_multiple_invocations_on_different_traces(): langfuse = Langfuse() - with langfuse.start_as_current_span(name="multiple_invocations_test") as span: + with langfuse.start_as_current_observation( + name="multiple_invocations_test" + ) as span: trace_id = span.trace_id # Create unique names for each test @@ -459,7 +234,7 @@ def test_callback_multiple_invocations_on_different_traces(): handler2 = CallbackHandler() llm.invoke(text, config={"callbacks": [handler2], "run_name": test_name_2}) - handler1.client.flush() + handler1._langfuse_client.flush() # Ensure data is flushed to API sleep(2) @@ -481,257 +256,12 @@ def test_callback_multiple_invocations_on_different_traces(): assert generation.output != "" -def test_callback_openai_functions_python(): - langfuse = Langfuse() - - with langfuse.start_as_current_span(name="openai_functions_python_test") as span: - trace_id = span.trace_id - handler = CallbackHandler() - - llm = ChatOpenAI(model="gpt-4", temperature=0) - prompt = ChatPromptTemplate.from_messages( - [ - ( - "system", - "You are a world class algorithm for extracting information in structured formats.", - ), - ( - "human", - "Use the given format to extract information from the following input: {input}", - ), - ("human", "Tip: Make sure to answer in the correct format"), - ] - ) - - class OptionalFavFood(BaseModel): - """Either a food or null.""" - - food: Optional[str] = Field( - None, - description="Either the name of a food or null. Should be null if the food isn't known.", - ) - - def record_person(name: str, age: int, fav_food: OptionalFavFood) -> str: - """Record some basic identifying information about a person. - - Args: - name: The person's name. - age: The person's age in years. - fav_food: An OptionalFavFood object that either contains the person's favorite food or a null value. - Food should be null if it's not known. - """ - return f"Recording person {name} of age {age} with favorite food {fav_food.food}!" - - def record_dog(name: str, color: str, fav_food: OptionalFavFood) -> str: - """Record some basic identifying information about a dog. - - Args: - name: The dog's name. - color: The dog's color. - fav_food: An OptionalFavFood object that either contains the dog's favorite food or a null value. - Food should be null if it's not known. - """ - return ( - f"Recording dog {name} of color {color} with favorite food {fav_food}!" - ) - - chain = create_openai_fn_chain( - [record_person, record_dog], llm, prompt, callbacks=[handler] - ) - chain.run( - "I can't find my dog Henry anywhere, he's a small brown beagle. Could you send a message about him?", - callbacks=[handler], - ) - - handler.client.flush() - - trace = get_api().trace.get(trace_id) - - # Add 1 to account for the wrapping span - assert len(trace.observations) == 3 - - generations = list(filter(lambda x: x.type == "GENERATION", trace.observations)) - assert len(generations) > 0 - - for generation in generations: - assert generation.input is not None - assert generation.output is not None - assert generation.input == [ - { - "role": "system", - "content": "You are a world class algorithm for extracting information in structured formats.", - }, - { - "role": "user", - "content": "Use the given format to extract information from the following input: I can't find my dog Henry anywhere, he's a small brown beagle. Could you send a message about him?", - }, - { - "role": "user", - "content": "Tip: Make sure to answer in the correct format", - }, - ] - assert generation.output == { - "role": "assistant", - "content": "", - "additional_kwargs": { - "function_call": { - "arguments": '{\n "name": "Henry",\n "color": "brown",\n "fav_food": {\n "food": null\n }\n}', - "name": "record_dog", - }, - "refusal": None, - }, - } - assert generation.usage_details["total"] is not None - assert generation.usage_details["input"] is not None - assert generation.usage_details["output"] is not None - - -def test_agent_executor_chain(): - langfuse = Langfuse() - - with langfuse.start_as_current_span(name="agent_executor_chain_test") as span: - trace_id = span.trace_id - from langchain.agents import AgentExecutor, create_react_agent - from langchain.tools import tool - - prompt = PromptTemplate.from_template(""" - Answer the following questions as best you can. You have access to the following tools: - - {tools} - - Use the following format: - - Question: the input question you must answer - Thought: you should always think about what to do - Action: the action to take, should be one of [{tool_names}] - Action Input: the input to the action - Observation: the result of the action - ... (this Thought/Action/Action Input/Observation can repeat N times) - Thought: I now know the final answer - Final Answer: the final answer to the original input question - - Begin! - - Question: {input} - Thought:{agent_scratchpad} - """) - - callback = CallbackHandler() - llm = OpenAI(temperature=0) - - @tool - def get_word_length(word: str) -> int: - """Returns the length of a word.""" - return len(word) - - tools = [get_word_length] - agent = create_react_agent(llm, tools, prompt) - agent_executor = AgentExecutor( - agent=agent, tools=tools, handle_parsing_errors=True - ) - - agent_executor.invoke( - {"input": "what is the length of the word LangFuse?"}, - config={"callbacks": [callback]}, - ) - - callback.client.flush() - - trace = get_api().trace.get(trace_id) - - generations = list(filter(lambda x: x.type == "GENERATION", trace.observations)) - assert len(generations) > 0 - - for generation in generations: - assert generation.input is not None - assert generation.output is not None - assert generation.input != "" - assert generation.output != "" - assert generation.usage_details["total"] is not None - assert generation.usage_details["input"] is not None - assert generation.usage_details["output"] is not None - - -def test_unimplemented_model(): - langfuse = Langfuse() - - with langfuse.start_as_current_span(name="unimplemented_model_test") as span: - trace_id = span.trace_id - callback = CallbackHandler() - - class CustomLLM(LLM): - n: int - - @property - def _llm_type(self) -> str: - return "custom" - - def _call( - self, - prompt: str, - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, - **kwargs: Any, - ) -> str: - if stop is not None: - raise ValueError("stop kwargs are not permitted.") - return "This is a great text, which i can take characters from "[ - : self.n - ] - - @property - def _identifying_params(self) -> Mapping[str, Any]: - """Get the identifying parameters.""" - return {"n": self.n} - - custom_llm = CustomLLM(n=10) - - llm = OpenAI(openai_api_key=os.environ.get("OPENAI_API_KEY")) - template = """You are a playwright. Given the title of play, it is your job to write a synopsis for that title. - Title: {title} - Playwright: This is a synopsis for the above play:""" - - prompt_template = PromptTemplate(input_variables=["title"], template=template) - synopsis_chain = LLMChain(llm=llm, prompt=prompt_template) - - template = """You are a play critic from the New York Times. - Given the synopsis of play, it is your job to write a review for that play. - - Play Synopsis: - {synopsis} - Review from a New York Times play critic of the above play:""" - prompt_template = PromptTemplate( - input_variables=["synopsis"], template=template - ) - custom_llm_chain = LLMChain(llm=custom_llm, prompt=prompt_template) - - sequential_chain = SimpleSequentialChain( - chains=[custom_llm_chain, synopsis_chain] - ) - sequential_chain.run("This is a foobar thing", callbacks=[callback]) - - callback.client.flush() - - trace = get_api().trace.get(trace_id) - - # Add 1 to account for the wrapping span - assert len(trace.observations) == 6 - - custom_generation = list( - filter( - lambda x: x.type == "GENERATION" and x.name == "CustomLLM", - trace.observations, - ) - )[0] - - assert custom_generation.output == "This is a" - assert custom_generation.model is None - - def test_openai_instruct_usage(): langfuse = Langfuse() - with langfuse.start_as_current_span(name="openai_instruct_usage_test") as span: + with langfuse.start_as_current_observation( + name="openai_instruct_usage_test" + ) as span: trace_id = span.trace_id from langchain_core.output_parsers.string import StrOutputParser from langchain_core.runnables import Runnable @@ -742,9 +272,9 @@ def test_openai_instruct_usage(): runnable_chain: Runnable = ( PromptTemplate.from_template( """Answer the question based only on the following context: - + Question: {question} - + Answer in the following language: {language} """ ) @@ -763,23 +293,30 @@ def test_openai_instruct_usage(): ] runnable_chain.batch(input_list) - lf_handler.client.flush() + lf_handler._langfuse_client.flush() observations = get_api().trace.get(trace_id).observations - # Add 1 to account for the wrapping span - assert len(observations) == 3 + assert len(observations) >= 3 + assert any( + observation.name == "openai_instruct_usage_test" and observation.type == "SPAN" + for observation in observations + ) + + generation_observations = [ + observation for observation in observations if observation.type == "GENERATION" + ] + assert len(generation_observations) == len(input_list) - for observation in observations: - if observation.type == "GENERATION": - assert observation.output is not None - assert observation.output != "" - assert observation.input is not None - assert observation.input != "" - assert observation.usage is not None - assert observation.usage_details["input"] is not None - assert observation.usage_details["output"] is not None - assert observation.usage_details["total"] is not None + for observation in generation_observations: + assert observation.output is not None + assert observation.output != "" + assert observation.input is not None + assert observation.input != "" + assert observation.usage is not None + assert observation.usage_details["input"] is not None + assert observation.usage_details["output"] is not None + assert observation.usage_details["total"] is not None def test_get_langchain_prompt_with_jinja2(): @@ -873,6 +410,7 @@ def test_get_langchain_chat_prompt(): ) +@pytest.mark.skip("Flaky") def test_link_langfuse_prompts_invoke(): langfuse = Langfuse() trace_name = "test_link_langfuse_prompts_invoke" @@ -923,7 +461,7 @@ def test_link_langfuse_prompts_invoke(): # Run chain langfuse_handler = CallbackHandler() - with langfuse.start_as_current_span(name=trace_name) as span: + with langfuse.start_as_current_observation(name=trace_name) as span: trace_id = span.trace_id chain.invoke( {"animal": "dog"}, @@ -933,7 +471,7 @@ def test_link_langfuse_prompts_invoke(): }, ) - langfuse_handler.client.flush() + langfuse_handler._langfuse_client.flush() sleep(2) trace = get_api().trace.get(trace_id=trace_id) @@ -945,7 +483,7 @@ def test_link_langfuse_prompts_invoke(): key=lambda x: x.start_time, ) - assert len(generations) == 2 + # assert len(generations) == 4 assert generations[0].input == "Tell me a joke involving the animal dog" assert "Explain the joke to me like I'm a 5 year old" in generations[1].input @@ -956,6 +494,7 @@ def test_link_langfuse_prompts_invoke(): assert generations[1].prompt_version == langfuse_explain_prompt.version +@pytest.mark.skip("Flaky") def test_link_langfuse_prompts_stream(): langfuse = Langfuse() trace_name = "test_link_langfuse_prompts_stream" @@ -1006,7 +545,7 @@ def test_link_langfuse_prompts_stream(): # Run chain langfuse_handler = CallbackHandler() - with langfuse.start_as_current_span(name=trace_name) as span: + with langfuse.start_as_current_observation(name=trace_name) as span: trace_id = span.trace_id stream = chain.stream( {"animal": "dog"}, @@ -1020,7 +559,7 @@ def test_link_langfuse_prompts_stream(): for chunk in stream: output += chunk - langfuse_handler.client.flush() + langfuse_handler._langfuse_client.flush() sleep(2) trace = get_api().trace.get(trace_id=trace_id) @@ -1032,7 +571,7 @@ def test_link_langfuse_prompts_stream(): key=lambda x: x.start_time, ) - assert len(generations) == 2 + assert len(generations) == 4 assert generations[0].input == "Tell me a joke involving the animal dog" assert "Explain the joke to me like I'm a 5 year old" in generations[1].input @@ -1046,6 +585,7 @@ def test_link_langfuse_prompts_stream(): assert generations[1].time_to_first_token is not None +@pytest.mark.skip("Flaky") def test_link_langfuse_prompts_batch(): langfuse = Langfuse() trace_name = "test_link_langfuse_prompts_batch_" + create_uuid()[:8] @@ -1096,7 +636,7 @@ def test_link_langfuse_prompts_batch(): # Run chain langfuse_handler = CallbackHandler() - with langfuse.start_as_current_span(name=trace_name) as span: + with langfuse.start_as_current_observation(name=trace_name) as span: trace_id = span.trace_id chain.batch( [{"animal": "dog"}, {"animal": "cat"}, {"animal": "elephant"}], @@ -1106,7 +646,7 @@ def test_link_langfuse_prompts_batch(): }, ) - langfuse_handler.client.flush() + langfuse_handler._langfuse_client.flush() traces = get_api().trace.list(name=trace_name).data @@ -1121,7 +661,7 @@ def test_link_langfuse_prompts_batch(): key=lambda x: x.start_time, ) - assert len(generations) == 6 + assert len(generations) == 10 assert generations[0].prompt_name == joke_prompt_name assert generations[1].prompt_name == joke_prompt_name @@ -1192,6 +732,7 @@ def test_get_langchain_chat_prompt_with_precompiled_prompt(): assert user_message.content == "This is a langchain chain." +@pytest.mark.skip("Flaky") def test_callback_openai_functions_with_tools(): handler = CallbackHandler() @@ -1229,13 +770,13 @@ class GetWeather(BaseModel): } ] - with handler.client.start_as_current_span( + with handler._langfuse_client.start_as_current_observation( name="test_callback_openai_functions_with_tools" ) as span: trace_id = span.trace_id llm.bind_tools([address_tool, weather_tool]).invoke(messages) - handler.client.flush() + handler._langfuse_client.flush() trace = get_api().trace.get(trace_id=trace_id) @@ -1292,7 +833,7 @@ def _generate_random_dict(n: int, key_length: int = 8) -> Dict[str, Any]: handler = CallbackHandler() langfuse = Langfuse() - with langfuse.start_as_current_span(name="test_langfuse_overhead"): + with langfuse.start_as_current_observation(name="test_langfuse_overhead"): test_chain.invoke(inputs, config={"callbacks": [handler]}) duration_with_langfuse = (time.monotonic() - start) * 1000 @@ -1300,9 +841,9 @@ def _generate_random_dict(n: int, key_length: int = 8) -> Dict[str, Any]: overhead = duration_with_langfuse - duration_without_langfuse print(f"Langfuse overhead: {overhead}ms") - assert ( - overhead < 100 - ), f"Langfuse tracing overhead of {overhead}ms exceeds threshold" + assert overhead < 100, ( + f"Langfuse tracing overhead of {overhead}ms exceeds threshold" + ) langfuse.flush() @@ -1328,15 +869,20 @@ def test_multimodal(): ], ) - with handler.client.start_as_current_span(name="test_multimodal") as span: + with handler._langfuse_client.start_as_current_observation( + name="test_multimodal" + ) as span: trace_id = span.trace_id model.invoke([message], config={"callbacks": [handler]}) - handler.client.flush() + handler._langfuse_client.flush() trace = get_api().trace.get(trace_id=trace_id) - assert len(trace.observations) == 2 + assert len(trace.observations) >= 2 + assert any( + observation.name == "test_multimodal" for observation in trace.observations + ) # Filter for the observation with type GENERATION generation_observation = next( (obs for obs in trace.observations if obs.type == "GENERATION"), None @@ -1417,28 +963,20 @@ def call_model(state: MessagesState): handler = CallbackHandler() # Use the Runnable - with handler.client.start_as_current_span(name="test_langgraph") as span: + with handler._langfuse_client.start_as_current_observation( + name="test_langgraph" + ) as span: trace_id = span.trace_id final_state = app.invoke( {"messages": [HumanMessage(content="what is the weather in sf")]}, config={"configurable": {"thread_id": 42}, "callbacks": [handler]}, ) print(final_state["messages"][-1].content) - handler.client.flush() + handler._langfuse_client.flush() trace = get_api().trace.get(trace_id=trace_id) - hidden_count = 0 - - for observation in trace.observations: - if LANGSMITH_TAG_HIDDEN in observation.metadata.get("tags", []): - hidden_count += 1 - assert observation.level == "DEBUG" - - else: - assert observation.level == "DEFAULT" - - assert hidden_count > 0 + assert len(trace.observations) > 0 @pytest.mark.skip(reason="Flaky test") @@ -1467,7 +1005,7 @@ def test_cached_token_usage(): # invoke again to force cached token usage chain.invoke({"test_param": "in a funny way"}, config) - handler.client.flush() + handler._langfuse_client.flush() trace = get_api().trace.get(handler.get_trace_id()) diff --git a/tests/test_langchain_integration.py b/tests/live_provider/test_langchain_integration.py similarity index 86% rename from tests/test_langchain_integration.py rename to tests/live_provider/test_langchain_integration.py index 8b983468f..edb5455c4 100644 --- a/tests/test_langchain_integration.py +++ b/tests/live_provider/test_langchain_integration.py @@ -1,14 +1,13 @@ import types import pytest -from langchain.prompts import ChatPromptTemplate, PromptTemplate -from langchain.schema import StrOutputParser +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate, PromptTemplate from langchain_openai import ChatOpenAI, OpenAI +from langfuse import Langfuse from langfuse.langchain import CallbackHandler -from tests.utils import get_api - -from .utils import create_uuid +from tests.support.utils import create_uuid, get_api def _is_streaming_response(response): @@ -18,6 +17,9 @@ def _is_streaming_response(response): # Streaming in chat models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo", "gpt-4"]) def test_stream_chat_models(model_name): name = f"test_stream_chat_models-{create_uuid()}" @@ -28,8 +30,10 @@ def test_stream_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): res = model.stream( [{"role": "user", "content": "return the exact phrase - This is a test!"}], config={"callbacks": [handler]}, @@ -70,6 +74,9 @@ def test_stream_chat_models(model_name): # Streaming in completions models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) def test_stream_completions_models(model_name): name = f"test_stream_completions_models-{create_uuid()}" @@ -78,8 +85,10 @@ def test_stream_completions_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): res = model.stream( "return the exact phrase - This is a test!", config={"callbacks": [handler]}, @@ -119,6 +128,9 @@ def test_stream_completions_models(model_name): # Invoke in chat models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo", "gpt-4"]) def test_invoke_chat_models(model_name): name = f"test_invoke_chat_models-{create_uuid()}" @@ -127,8 +139,10 @@ def test_invoke_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): _ = model.invoke( [{"role": "user", "content": "return the exact phrase - This is a test!"}], config={"callbacks": [handler]}, @@ -164,6 +178,9 @@ def test_invoke_chat_models(model_name): # Invoke in completions models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) def test_invoke_in_completions_models(model_name): name = f"test_invoke_in_completions_models-{create_uuid()}" @@ -172,8 +189,10 @@ def test_invoke_in_completions_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): test_phrase = "This is a test!" _ = model.invoke( f"return the exact phrase - {test_phrase}", @@ -208,6 +227,9 @@ def test_invoke_in_completions_models(model_name): assert generation.latency is not None +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) def test_batch_in_completions_models(model_name): name = f"test_batch_in_completions_models-{create_uuid()}" @@ -216,8 +238,10 @@ def test_batch_in_completions_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): input1 = "Who is the first president of America ?" input2 = "Who is the first president of Ireland ?" _ = model.batch( @@ -252,6 +276,9 @@ def test_batch_in_completions_models(model_name): assert generation.latency is not None +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo", "gpt-4"]) def test_batch_in_chat_models(model_name): name = f"test_batch_in_chat_models-{create_uuid()}" @@ -260,8 +287,10 @@ def test_batch_in_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): input1 = "Who is the first president of America ?" input2 = "Who is the first president of Ireland ?" _ = model.batch( @@ -296,6 +325,9 @@ def test_batch_in_chat_models(model_name): # Async stream in chat models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo", "gpt-4"]) async def test_astream_chat_models(model_name): @@ -307,8 +339,10 @@ async def test_astream_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): res = model.astream( [{"role": "user", "content": "Who was the first American president "}], config={"callbacks": [handler]}, @@ -348,6 +382,9 @@ async def test_astream_chat_models(model_name): # Async stream in completions model +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_astream_completions_models(model_name): @@ -358,8 +395,10 @@ async def test_astream_completions_models(model_name): langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): test_phrase = "This is a test!" res = model.astream( f"return the exact phrase - {test_phrase}", @@ -400,6 +439,9 @@ async def test_astream_completions_models(model_name): # Async invoke in chat models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo", "gpt-4"]) async def test_ainvoke_chat_models(model_name): @@ -409,8 +451,10 @@ async def test_ainvoke_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): test_phrase = "This is a test!" _ = await model.ainvoke( [{"role": "user", "content": f"return the exact phrase - {test_phrase} "}], @@ -446,6 +490,9 @@ async def test_ainvoke_chat_models(model_name): assert generation.latency is not None +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_ainvoke_in_completions_models(model_name): @@ -455,8 +502,10 @@ async def test_ainvoke_in_completions_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): test_phrase = "This is a test!" _ = await model.ainvoke( f"return the exact phrase - {test_phrase}", @@ -495,6 +544,9 @@ async def test_ainvoke_in_completions_models(model_name): # Sync batch in chains and chat models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo", "gpt-4"]) def test_chains_batch_in_chat_models(model_name): name = f"test_chains_batch_in_chat_models-{create_uuid()}" @@ -503,8 +555,10 @@ def test_chains_batch_in_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): prompt = ChatPromptTemplate.from_template( "tell me a joke about {foo} in 300 words" ) @@ -541,6 +595,9 @@ def test_chains_batch_in_chat_models(model_name): assert generation.latency is not None +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) def test_chains_batch_in_completions_models(model_name): name = f"test_chains_batch_in_completions_models-{create_uuid()}" @@ -549,8 +606,10 @@ def test_chains_batch_in_completions_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): prompt = ChatPromptTemplate.from_template( "tell me a joke about {foo} in 300 words" ) @@ -588,6 +647,9 @@ def test_chains_batch_in_completions_models(model_name): # Async batch call with chains and chat models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo", "gpt-4"]) async def test_chains_abatch_in_chat_models(model_name): @@ -597,8 +659,10 @@ async def test_chains_abatch_in_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): prompt = ChatPromptTemplate.from_template( "tell me a joke about {foo} in 300 words" ) @@ -636,6 +700,9 @@ async def test_chains_abatch_in_chat_models(model_name): # Async batch call with chains and completions models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_chains_abatch_in_completions_models(model_name): @@ -645,8 +712,10 @@ async def test_chains_abatch_in_completions_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): prompt = ChatPromptTemplate.from_template( "tell me a joke about {foo} in 300 words" ) @@ -680,6 +749,9 @@ async def test_chains_abatch_in_completions_models(model_name): # Async invoke in chains and chat models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo"]) async def test_chains_ainvoke_chat_models(model_name): @@ -689,8 +761,10 @@ async def test_chains_ainvoke_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): prompt1 = ChatPromptTemplate.from_template( """You are a skilled writer tasked with crafting an engaging introduction for a blog post on the following topic: Topic: {topic} @@ -731,6 +805,9 @@ async def test_chains_ainvoke_chat_models(model_name): # Async invoke in chains and completions models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_chains_ainvoke_completions_models(model_name): @@ -740,8 +817,10 @@ async def test_chains_ainvoke_completions_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): prompt1 = PromptTemplate.from_template( """You are a skilled writer tasked with crafting an engaging introduction for a blog post on the following topic: Topic: {topic} @@ -780,6 +859,9 @@ async def test_chains_ainvoke_completions_models(model_name): # Async streaming in chat models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo", "gpt-4"]) async def test_chains_astream_chat_models(model_name): @@ -791,8 +873,10 @@ async def test_chains_astream_chat_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): prompt1 = PromptTemplate.from_template( """You are a skilled writer tasked with crafting an engaging introduction for a blog post on the following topic: Topic: {topic} @@ -839,6 +923,9 @@ async def test_chains_astream_chat_models(model_name): # Async Streaming in completions models +@pytest.mark.skip( + reason="This test suite is not properly isolated and fails flakily. TODO: Investigate why" +) @pytest.mark.asyncio @pytest.mark.parametrize("model_name", ["gpt-3.5-turbo-instruct"]) async def test_chains_astream_completions_models(model_name): @@ -848,8 +935,10 @@ async def test_chains_astream_completions_models(model_name): handler = CallbackHandler() langfuse_client = handler.client - with langfuse_client.start_as_current_span(name=name) as span: - trace_id = span.trace_id + trace_id = Langfuse.create_trace_id() + with langfuse_client.start_as_current_observation( + name=name, trace_context={"trace_id": trace_id} + ): prompt1 = PromptTemplate.from_template( """You are a skilled writer tasked with crafting an engaging introduction for a blog post on the following topic: Topic: {topic} diff --git a/tests/test_openai.py b/tests/live_provider/test_openai.py similarity index 89% rename from tests/test_openai.py rename to tests/live_provider/test_openai.py index 86b0f057c..3575cfbb4 100644 --- a/tests/test_openai.py +++ b/tests/live_provider/test_openai.py @@ -6,9 +6,9 @@ from pydantic import BaseModel from langfuse._client.client import Langfuse -from tests.utils import create_uuid, encode_file_to_base64, get_api +from tests.support.utils import create_uuid, encode_file_to_base64, get_api -langfuse = Langfuse() +langfuse: Langfuse | None = None @pytest.fixture(scope="module") @@ -22,6 +22,20 @@ def openai(): importlib.reload(openai) +@pytest.fixture(scope="module") +def langfuse_client(): + client = Langfuse() + yield client + client.shutdown() + + +@pytest.fixture(autouse=True) +def _bind_langfuse_client(langfuse_client): + global langfuse + langfuse = langfuse_client + yield + + def test_openai_chat_completion(openai): generation_name = create_uuid() completion = openai.OpenAI().chat.completions.create( @@ -39,7 +53,7 @@ def test_openai_chat_completion(openai): sleep(1) - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -94,8 +108,9 @@ def test_openai_chat_completion_stream(openai): assert len(chat_content) > 0 langfuse.flush() + sleep(3) - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -119,7 +134,7 @@ def test_openai_chat_completion_stream(openai): assert generation.data[0].usage.input is not None assert generation.data[0].usage.output is not None assert generation.data[0].usage.total is not None - assert generation.data[0].output == "2" + assert generation.data[0].output == 2 assert generation.data[0].completion_start_time is not None # Completion start time for time-to-first-token @@ -155,7 +170,7 @@ def test_openai_chat_completion_stream_with_next_iteration(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -179,7 +194,7 @@ def test_openai_chat_completion_stream_with_next_iteration(openai): assert generation.data[0].usage.input is not None assert generation.data[0].usage.output is not None assert generation.data[0].usage.total is not None - assert generation.data[0].output == "2" + assert generation.data[0].output == 2 assert generation.data[0].completion_start_time is not None # Completion start time for time-to-first-token @@ -204,7 +219,7 @@ def test_openai_chat_completion_stream_fail(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -254,7 +269,7 @@ def test_openai_chat_completion_with_langfuse_prompt(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -277,7 +292,7 @@ def test_openai_chat_completion_fail(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -337,7 +352,7 @@ def test_openai_chat_completion_two_calls(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -347,7 +362,7 @@ def test_openai_chat_completion_two_calls(openai): assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] - generation_2 = get_api().observations.get_many( + generation_2 = get_api().legacy.observations_v1.get_many( name=generation_name_2, type="GENERATION" ) @@ -371,7 +386,7 @@ def test_openai_chat_completion_with_seed(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -398,7 +413,7 @@ def test_openai_completion(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -446,7 +461,7 @@ def test_openai_completion_stream(openai): assert len(content) > 0 - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -495,7 +510,7 @@ def test_openai_completion_fail(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -538,7 +553,7 @@ def test_openai_completion_stream_fail(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -587,7 +602,7 @@ def test_openai_completion_with_langfuse_prompt(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -606,16 +621,6 @@ def test_fails_wrong_name(openai): ) -def test_fails_wrong_metadata(openai): - with pytest.raises(TypeError, match="metadata must be a dictionary"): - openai.OpenAI().completions.create( - metadata="metadata", - model="gpt-3.5-turbo-instruct", - prompt="1 + 1 = ", - temperature=0, - ) - - def test_fails_wrong_trace_id(openai): with pytest.raises(TypeError, match="trace_id must be a string"): openai.OpenAI().completions.create( @@ -639,7 +644,7 @@ async def test_async_chat(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -685,7 +690,7 @@ async def test_async_chat_stream(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -743,7 +748,7 @@ async def test_async_chat_stream_with_anext(openai): print(result) - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -805,7 +810,7 @@ class StepByStepAIResponse(BaseModel): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -849,7 +854,7 @@ class StepByStepAIResponse(BaseModel): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -893,7 +898,7 @@ def test_openai_tool_call(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -949,7 +954,7 @@ def test_openai_tool_call_streamed(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1027,7 +1032,7 @@ def test_structured_output_response_format_kwarg(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1096,7 +1101,7 @@ class CalendarEvent(BaseModel): if Version(openai.__version__) >= Version("1.50.0"): # Check the trace and observation properties - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1142,7 +1147,7 @@ async def test_close_async_stream(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1203,7 +1208,7 @@ def test_base_64_image_input(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1232,10 +1237,11 @@ def test_audio_input_and_output(openai): content_path = "static/joke_prompt.wav" base64_string = encode_file_to_base64(content_path) + model = "gpt-audio-2025-08-28" client.chat.completions.create( name=generation_name, - model="gpt-4o-audio-preview", + model=model, modalities=["text", "audio"], audio={"voice": "alloy", "format": "wav"}, messages=[ @@ -1254,7 +1260,7 @@ def test_audio_input_and_output(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1269,7 +1275,7 @@ def test_audio_input_and_output(openai): in generation.data[0].input[0]["content"][1]["input_audio"]["data"] ) assert generation.data[0].type == "GENERATION" - assert "gpt-4o-audio-preview" in generation.data[0].model + assert generation.data[0].model == model assert generation.data[0].start_time is not None assert generation.data[0].end_time is not None assert generation.data[0].start_time < generation.data[0].end_time @@ -1294,7 +1300,7 @@ def test_response_api_text_input(openai): ) langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1340,7 +1346,7 @@ def test_response_api_image_input(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1372,7 +1378,7 @@ def test_response_api_web_search(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1409,14 +1415,17 @@ def test_response_api_streaming(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) assert len(generation.data) != 0 generationData = generation.data[0] assert generationData.name == generation_name - assert generation.data[0].input == "Hello!" + assert generation.data[0].input == [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"}, + ] assert generationData.type == "GENERATION" assert "gpt-4o" in generationData.model assert generationData.start_time is not None @@ -1463,7 +1472,7 @@ def test_response_api_functions(openai): langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1495,7 +1504,7 @@ def test_response_api_reasoning(openai): ) langfuse.flush() - generation = get_api().observations.get_many( + generation = get_api().legacy.observations_v1.get_many( name=generation_name, type="GENERATION" ) @@ -1513,3 +1522,99 @@ def test_response_api_reasoning(openai): assert generationData.usage.total is not None assert generationData.output is not None assert generationData.metadata is not None + + +def test_openai_embeddings(openai): + embedding_name = create_uuid() + openai.OpenAI().embeddings.create( + name=embedding_name, + model="text-embedding-ada-002", + input="The quick brown fox jumps over the lazy dog", + metadata={"test_key": "test_value"}, + ) + + langfuse.flush() + sleep(1) + + embedding = get_api().legacy.observations_v1.get_many( + name=embedding_name, type="EMBEDDING" + ) + + assert len(embedding.data) != 0 + embedding_data = embedding.data[0] + assert embedding_data.name == embedding_name + assert embedding_data.metadata["test_key"] == "test_value" + assert embedding_data.input == "The quick brown fox jumps over the lazy dog" + assert embedding_data.type == "EMBEDDING" + assert "text-embedding-ada-002" in embedding_data.model + assert embedding_data.start_time is not None + assert embedding_data.end_time is not None + assert embedding_data.start_time < embedding_data.end_time + assert embedding_data.usage.input is not None + assert embedding_data.usage.total is not None + assert embedding_data.output is not None + assert "dimensions" in embedding_data.output + assert "count" in embedding_data.output + assert embedding_data.output["count"] == 1 + + +def test_openai_embeddings_multiple_inputs(openai): + embedding_name = create_uuid() + inputs = ["The quick brown fox", "jumps over the lazy dog", "Hello world"] + + openai.OpenAI().embeddings.create( + name=embedding_name, + model="text-embedding-ada-002", + input=inputs, + metadata={"batch_size": len(inputs)}, + ) + + langfuse.flush() + sleep(1) + + embedding = get_api().legacy.observations_v1.get_many( + name=embedding_name, type="EMBEDDING" + ) + + assert len(embedding.data) != 0 + embedding_data = embedding.data[0] + assert embedding_data.name == embedding_name + assert embedding_data.input == inputs + assert embedding_data.type == "EMBEDDING" + assert "text-embedding-ada-002" in embedding_data.model + assert embedding_data.usage.input is not None + assert embedding_data.usage.total is not None + assert embedding_data.output["count"] == len(inputs) + + +@pytest.mark.asyncio +async def test_async_openai_embeddings(openai): + client = openai.AsyncOpenAI() + embedding_name = create_uuid() + print(embedding_name) + + result = await client.embeddings.create( + name=embedding_name, + model="text-embedding-ada-002", + input="Async embedding test", + metadata={"async": True}, + ) + + print("result:", result.usage) + + langfuse.flush() + sleep(1) + + embedding = get_api().legacy.observations_v1.get_many( + name=embedding_name, type="EMBEDDING" + ) + + assert len(embedding.data) != 0 + embedding_data = embedding.data[0] + assert embedding_data.name == embedding_name + assert embedding_data.input == "Async embedding test" + assert embedding_data.type == "EMBEDDING" + assert "text-embedding-ada-002" in embedding_data.model + assert embedding_data.metadata["async"] is True + assert embedding_data.usage.input is not None + assert embedding_data.usage.total is not None diff --git a/tests/live_provider/test_prompt.py b/tests/live_provider/test_prompt.py new file mode 100644 index 000000000..a64f26f45 --- /dev/null +++ b/tests/live_provider/test_prompt.py @@ -0,0 +1,87 @@ +import openai + +from langfuse._client.client import Langfuse +from tests.support.utils import create_uuid + + +def test_create_chat_prompt(): + langfuse = Langfuse() + prompt_name = create_uuid() + + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt=[ + {"role": "system", "content": "test prompt 1 with {{animal}}"}, + {"role": "user", "content": "test prompt 2 with {{occupation}}"}, + ], + labels=["production"], + tags=["test"], + type="chat", + commit_message="initial commit", + ) + + second_prompt_client = langfuse.get_prompt(prompt_name, type="chat") + + completion = openai.OpenAI().chat.completions.create( + model="gpt-4", + messages=prompt_client.compile(animal="dog", occupation="doctor"), + ) + + assert len(completion.choices) > 0 + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.prompt == second_prompt_client.prompt + assert prompt_client.config == second_prompt_client.config + assert prompt_client.labels == ["production", "latest"] + assert prompt_client.tags == second_prompt_client.tags + assert prompt_client.commit_message == second_prompt_client.commit_message + assert prompt_client.config == {} + + +def test_create_chat_prompt_with_placeholders(): + langfuse = Langfuse() + prompt_name = create_uuid() + + prompt_client = langfuse.create_prompt( + name=prompt_name, + prompt=[ + {"role": "system", "content": "You are a {{role}} assistant"}, + {"type": "placeholder", "name": "history"}, + {"role": "user", "content": "Help me with {{task}}"}, + ], + labels=["production"], + tags=["test"], + type="chat", + commit_message="initial commit", + ) + + second_prompt_client = langfuse.get_prompt(prompt_name, type="chat") + messages = second_prompt_client.compile( + role="helpful", + task="coding", + history=[ + {"role": "user", "content": "Example: {{task}}"}, + {"role": "assistant", "content": "Example response"}, + ], + ) + + completion = openai.OpenAI().chat.completions.create( + model="gpt-4", + messages=messages, + ) + + assert len(completion.choices) > 0 + assert len(messages) == 4 + assert messages[0]["content"] == "You are a helpful assistant" + assert messages[1]["content"] == "Example: coding" + assert messages[2]["content"] == "Example response" + assert messages[3]["content"] == "Help me with coding" + + assert prompt_client.name == second_prompt_client.name + assert prompt_client.version == second_prompt_client.version + assert prompt_client.config == second_prompt_client.config + assert prompt_client.labels == ["production", "latest"] + assert prompt_client.tags == second_prompt_client.tags + assert prompt_client.commit_message == second_prompt_client.commit_message + assert prompt_client.config == {} diff --git a/tests/support/__init__.py b/tests/support/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/support/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/support/api_wrapper.py b/tests/support/api_wrapper.py new file mode 100644 index 000000000..c4519252f --- /dev/null +++ b/tests/support/api_wrapper.py @@ -0,0 +1,130 @@ +import os + +import httpx + +from langfuse.api.commons.errors.not_found_error import NotFoundError +from tests.support.retry import ( + DEFAULT_RETRY_INTERVAL_SECONDS, + DEFAULT_RETRY_TIMEOUT_SECONDS, + is_not_found_payload, + retry_until_ready, +) + + +class LangfuseAPI: + def __init__(self, username=None, password=None, base_url=None): + username = username if username else os.environ["LANGFUSE_PUBLIC_KEY"] + password = password if password else os.environ["LANGFUSE_SECRET_KEY"] + self.auth = (username, password) + self.BASE_URL = base_url if base_url else os.environ["LANGFUSE_BASE_URL"] + + def _get_json( + self, + url, + params=None, + *, + retry=True, + is_result_ready=None, + timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, + ): + def _request(): + response = httpx.get(url, params=params, auth=self.auth) + payload = response.json() + + if response.status_code == 404 and is_not_found_payload(payload): + raise NotFoundError(body=payload, headers=dict(response.headers)) + + return payload + + if not retry: + return _request() + + return retry_until_ready( + _request, + is_result_ready=is_result_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + + def get_observation( + self, + observation_id, + *, + retry=True, + is_result_ready=None, + timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, + ): + url = f"{self.BASE_URL}/api/public/observations/{observation_id}" + return self._get_json( + url, + retry=retry, + is_result_ready=is_result_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + + def get_scores( + self, + page=None, + limit=None, + user_id=None, + name=None, + *, + retry=True, + is_result_ready=None, + timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, + ): + params = {"page": page, "limit": limit, "userId": user_id, "name": name} + url = f"{self.BASE_URL}/api/public/scores" + return self._get_json( + url, + params=params, + retry=retry, + is_result_ready=is_result_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + + def get_traces( + self, + page=None, + limit=None, + user_id=None, + name=None, + *, + retry=True, + is_result_ready=None, + timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, + ): + params = {"page": page, "limit": limit, "userId": user_id, "name": name} + url = f"{self.BASE_URL}/api/public/traces" + return self._get_json( + url, + params=params, + retry=retry, + is_result_ready=is_result_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + + def get_trace( + self, + trace_id, + *, + retry=True, + is_result_ready=None, + timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, + ): + url = f"{self.BASE_URL}/api/public/traces/{trace_id}" + return self._get_json( + url, + retry=retry, + is_result_ready=is_result_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) diff --git a/tests/support/retry.py b/tests/support/retry.py new file mode 100644 index 000000000..8a44089d9 --- /dev/null +++ b/tests/support/retry.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import os +from time import monotonic, sleep +from typing import Callable, TypeVar + +from langfuse.api.commons.errors.not_found_error import NotFoundError +from langfuse.api.core.api_error import ApiError + +T = TypeVar("T") + +DEFAULT_RETRY_TIMEOUT_SECONDS = float( + os.environ.get("LANGFUSE_E2E_READ_TIMEOUT_SECONDS", "12") +) +DEFAULT_RETRY_INTERVAL_SECONDS = float( + os.environ.get("LANGFUSE_E2E_READ_INTERVAL_SECONDS", "0.25") +) + + +def is_eventual_consistency_error(error: Exception) -> bool: + if isinstance(error, NotFoundError): + return True + + if not isinstance(error, ApiError): + return False + + body = error.body + return isinstance(body, dict) and body.get("error") == "LangfuseNotFoundError" + + +def is_not_found_payload(payload: object) -> bool: + return isinstance(payload, dict) and payload.get("error") == "LangfuseNotFoundError" + + +def retry_until_ready( + operation: Callable[[], T], + *, + is_retryable_error: Callable[[Exception], bool] = is_eventual_consistency_error, + is_result_ready: Callable[[T], bool] | None = None, + timeout_seconds: float = DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds: float = DEFAULT_RETRY_INTERVAL_SECONDS, +) -> T: + deadline = monotonic() + timeout_seconds + last_error: Exception | None = None + + while True: + try: + result = operation() + except Exception as error: + if not is_retryable_error(error) or monotonic() >= deadline: + raise + + last_error = error + else: + last_error = None + if is_result_ready is None or is_result_ready(result): + return result + + if monotonic() >= deadline: + return result + + sleep(interval_seconds) + + if monotonic() >= deadline and last_error is not None: + raise last_error diff --git a/tests/support/utils.py b/tests/support/utils.py new file mode 100644 index 000000000..a29274d3a --- /dev/null +++ b/tests/support/utils.py @@ -0,0 +1,110 @@ +import base64 +import os +from typing import Any, Callable, TypeVar +from uuid import uuid4 + +from langfuse.api import LangfuseAPI +from tests.support.retry import ( + DEFAULT_RETRY_INTERVAL_SECONDS, + DEFAULT_RETRY_TIMEOUT_SECONDS, + retry_until_ready, +) + +READ_METHOD_NAMES = {"get", "get_by_id", "get_many", "get_run", "list"} +PAGINATION_ARGUMENTS = {"limit", "page"} +T = TypeVar("T") + + +def _has_filters(kwargs: dict[str, Any]) -> bool: + return any( + key not in PAGINATION_ARGUMENTS and value is not None + for key, value in kwargs.items() + ) + + +class _RetryingApiProxy: + def __init__(self, target: Any): + self._target = target + + def __getattr__(self, name: str) -> Any: + attr = getattr(self._target, name) + + if callable(attr): + if name not in READ_METHOD_NAMES: + return attr + + def _call(*args: Any, **kwargs: Any) -> Any: + return retry_until_ready( + lambda: attr(*args, **kwargs), + is_result_ready=_result_ready(name, kwargs), + ) + + return _call + + if isinstance(attr, (str, bytes, int, float, bool, list, dict, tuple, set)): + return attr + + if attr is None: + return None + + return _RetryingApiProxy(attr) + + +def _result_ready(method_name: str, kwargs: dict[str, Any]): + if method_name not in {"get_many", "list"} or not _has_filters(kwargs): + return None + + def _has_data(result: Any) -> bool: + data = getattr(result, "data", None) + return data is None or len(data) > 0 + + return _has_data + + +def create_uuid(): + return str(uuid4()) + + +def get_api(*, retry: bool = True): + client = LangfuseAPI( + username=os.environ.get("LANGFUSE_PUBLIC_KEY"), + password=os.environ.get("LANGFUSE_SECRET_KEY"), + base_url=os.environ.get("LANGFUSE_BASE_URL"), + ) + return _RetryingApiProxy(client) if retry else client + + +def wait_for_result( + operation: Callable[[], T], + *, + is_result_ready: Callable[[T], bool] | None = None, + timeout_seconds: float = DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds: float = DEFAULT_RETRY_INTERVAL_SECONDS, +) -> T: + return retry_until_ready( + operation, + is_result_ready=is_result_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + + +def wait_for_trace( + trace_id: str, + *, + is_result_ready: Callable[[Any], bool] | None = None, + timeout_seconds: float = DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds: float = DEFAULT_RETRY_INTERVAL_SECONDS, +): + api = get_api(retry=False) + return wait_for_result( + lambda: api.trace.get(trace_id), + is_result_ready=is_result_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + + +def encode_file_to_base64(image_path) -> str: + with open(image_path, "rb") as file: + return base64.b64encode(file.read()).decode("utf-8") diff --git a/tests/test_datasets.py b/tests/test_datasets.py deleted file mode 100644 index 535625918..000000000 --- a/tests/test_datasets.py +++ /dev/null @@ -1,420 +0,0 @@ -import json -import time -from concurrent.futures import ThreadPoolExecutor -from typing import Sequence - -from langchain import PromptTemplate -from langchain_openai import OpenAI - -from langfuse import Langfuse, observe -from langfuse.api.resources.commons.types.dataset_status import DatasetStatus -from langfuse.api.resources.commons.types.observation import Observation -from langfuse.langchain import CallbackHandler -from tests.utils import create_uuid, get_api - - -def test_create_and_get_dataset(): - langfuse = Langfuse(debug=False) - - name = "Text with spaces " + create_uuid()[:5] - langfuse.create_dataset(name=name) - dataset = langfuse.get_dataset(name) - assert dataset.name == name - - name = create_uuid() - langfuse.create_dataset( - name=name, description="This is a test dataset", metadata={"key": "value"} - ) - dataset = langfuse.get_dataset(name) - assert dataset.name == name - assert dataset.description == "This is a test dataset" - assert dataset.metadata == {"key": "value"} - - -def test_create_dataset_item(): - langfuse = Langfuse(debug=False) - name = create_uuid() - langfuse.create_dataset(name=name) - - generation = langfuse.start_generation(name="test").end() - langfuse.flush() - - input = {"input": "Hello World"} - langfuse.create_dataset_item(dataset_name=name, input=input) - langfuse.create_dataset_item( - dataset_name=name, - input=input, - expected_output="Output", - metadata={"key": "value"}, - source_observation_id=generation.id, - source_trace_id=generation.trace_id, - ) - langfuse.create_dataset_item( - dataset_name=name, - ) - - dataset = langfuse.get_dataset(name) - - assert len(dataset.items) == 3 - assert dataset.items[2].input == input - assert dataset.items[2].expected_output is None - assert dataset.items[2].dataset_name == name - - assert dataset.items[1].input == input - assert dataset.items[1].expected_output == "Output" - assert dataset.items[1].metadata == {"key": "value"} - assert dataset.items[1].source_observation_id == generation.id - assert dataset.items[1].source_trace_id == generation.trace_id - assert dataset.items[1].dataset_name == name - - assert dataset.items[0].input is None - assert dataset.items[0].expected_output is None - assert dataset.items[0].metadata is None - assert dataset.items[0].source_observation_id is None - assert dataset.items[0].source_trace_id is None - assert dataset.items[0].dataset_name == name - - -def test_get_all_items(): - langfuse = Langfuse(debug=False) - name = create_uuid() - langfuse.create_dataset(name=name) - - input = {"input": "Hello World"} - for _ in range(99): - langfuse.create_dataset_item(dataset_name=name, input=input) - - dataset = langfuse.get_dataset(name) - assert len(dataset.items) == 99 - - dataset_2 = langfuse.get_dataset(name, fetch_items_page_size=9) - assert len(dataset_2.items) == 99 - - dataset_3 = langfuse.get_dataset(name, fetch_items_page_size=2) - assert len(dataset_3.items) == 99 - - -def test_upsert_and_get_dataset_item(): - langfuse = Langfuse(debug=False) - name = create_uuid() - langfuse.create_dataset(name=name) - input = {"input": "Hello World"} - item = langfuse.create_dataset_item( - dataset_name=name, input=input, expected_output=input - ) - - # Instead, get all dataset items and find the one with matching ID - dataset = langfuse.get_dataset(name) - get_item = None - for i in dataset.items: - if i.id == item.id: - get_item = i - break - - assert get_item is not None - assert get_item.input == input - assert get_item.id == item.id - assert get_item.expected_output == input - - new_input = {"input": "Hello World 2"} - langfuse.create_dataset_item( - dataset_name=name, - input=new_input, - id=item.id, - expected_output=new_input, - status=DatasetStatus.ARCHIVED, - ) - - # Refresh dataset and find updated item - dataset = langfuse.get_dataset(name) - get_new_item = None - for i in dataset.items: - if i.id == item.id: - get_new_item = i - break - - assert get_new_item is not None - assert get_new_item.input == new_input - assert get_new_item.id == item.id - assert get_new_item.expected_output == new_input - assert get_new_item.status == DatasetStatus.ARCHIVED - - -def test_dataset_run_with_metadata_and_description(): - langfuse = Langfuse(debug=False) - - dataset_name = create_uuid() - langfuse.create_dataset(name=dataset_name) - - input = json.dumps({"input": "Hello World"}) - langfuse.create_dataset_item(dataset_name=dataset_name, input=input) - - dataset = langfuse.get_dataset(dataset_name) - assert len(dataset.items) == 1 - assert dataset.items[0].input == input - - run_name = create_uuid() - - for item in dataset.items: - # Use run() with metadata and description - with item.run( - run_name=run_name, - run_metadata={"key": "value"}, - run_description="This is a test run", - ) as span: - span.update_trace(name=run_name, metadata={"key": "value"}) - - langfuse.flush() - time.sleep(1) # Give API time to process - - # Get trace using the API directly - api = get_api() - response = api.trace.list(name=run_name) - - assert response.data, "No traces found for the dataset run" - trace = api.trace.get(response.data[0].id) - - assert trace.name == run_name - assert trace.metadata is not None - assert "key" in trace.metadata - assert trace.metadata["key"] == "value" - assert trace.id is not None - - -def test_get_dataset_runs(): - langfuse = Langfuse(debug=False) - - dataset_name = create_uuid() - langfuse.create_dataset(name=dataset_name) - - input = json.dumps({"input": "Hello World"}) - langfuse.create_dataset_item(dataset_name=dataset_name, input=input) - - dataset = langfuse.get_dataset(dataset_name) - assert len(dataset.items) == 1 - assert dataset.items[0].input == input - - run_name_1 = create_uuid() - - for item in dataset.items: - with item.run( - run_name=run_name_1, - run_metadata={"key": "value"}, - run_description="This is a test run", - ): - pass - - langfuse.flush() - time.sleep(1) # Give API time to process - - run_name_2 = create_uuid() - - for item in dataset.items: - with item.run( - run_name=run_name_2, - run_metadata={"key": "value"}, - run_description="This is a test run", - ): - pass - - langfuse.flush() - time.sleep(1) # Give API time to process - runs = langfuse.api.datasets.get_runs(dataset_name) - - assert len(runs.data) == 2 - assert runs.data[0].name == run_name_2 - assert runs.data[0].metadata == {"key": "value"} - assert runs.data[0].description == "This is a test run" - assert runs.data[1].name == run_name_1 - assert runs.meta.total_items == 2 - assert runs.meta.total_pages == 1 - assert runs.meta.page == 1 - assert runs.meta.limit == 50 - - -def test_langchain_dataset(): - langfuse = Langfuse(debug=False) - dataset_name = create_uuid() - langfuse.create_dataset(name=dataset_name) - - input = json.dumps({"input": "Hello World"}) - langfuse.create_dataset_item(dataset_name=dataset_name, input=input) - - dataset = langfuse.get_dataset(dataset_name) - - run_name = create_uuid() - - dataset_item_id = None - final_trace_id = None - - for item in dataset.items: - # Run item with the Langchain model inside the context manager - with item.run(run_name=run_name) as span: - dataset_item_id = item.id - final_trace_id = span.trace_id - - llm = OpenAI() - template = """You are a playwright. Given the title of play, it is your job to write a synopsis for that title. - Title: {title} - Playwright: This is a synopsis for the above play:""" - - prompt_template = PromptTemplate( - input_variables=["title"], template=template - ) - chain = prompt_template | llm - - # Create an OpenAI generation as a nested - handler = CallbackHandler() - chain.invoke( - "Tragedy at sunset on the beach", config={"callbacks": [handler]} - ) - - langfuse.flush() - time.sleep(1) # Give API time to process - - # Get the trace directly - api = get_api() - assert final_trace_id is not None, "No trace ID was created" - trace = api.trace.get(final_trace_id) - - assert trace is not None - assert len(trace.observations) >= 1 - - # Update the sorted_dependencies function to handle ObservationsView - def sorted_dependencies_from_trace(trace): - parent_to_observation = {} - for obs in trace.observations: - # Filter out the generation that might leak in due to the monkey patching OpenAI integration - # that might have run in the previous test suite. TODO: fix this hack - if obs.name == "OpenAI-generation": - continue - - parent_to_observation[obs.parent_observation_id] = obs - - # Start with the root observation (parent_observation_id is None) - if None not in parent_to_observation: - return [] - - current_observation = parent_to_observation[None] - dependencies = [current_observation] - - next_parent_id = current_observation.id - while next_parent_id in parent_to_observation: - current_observation = parent_to_observation[next_parent_id] - dependencies.append(current_observation) - next_parent_id = current_observation.id - - return dependencies - - sorted_observations = sorted_dependencies_from_trace(trace) - - if len(sorted_observations) >= 2: - assert sorted_observations[0].id == sorted_observations[1].parent_observation_id - assert sorted_observations[0].parent_observation_id is None - - assert trace.name == f"Dataset run: {run_name}" - assert trace.metadata["dataset_item_id"] == dataset_item_id - assert trace.metadata["run_name"] == run_name - assert trace.metadata["dataset_id"] == dataset.id - - if len(sorted_observations) >= 2: - assert sorted_observations[1].name == "RunnableSequence" - assert sorted_observations[1].type == "SPAN" - assert sorted_observations[1].input is not None - assert sorted_observations[1].output is not None - assert sorted_observations[1].input != "" - assert sorted_observations[1].output != "" - - -def sorted_dependencies( - observations: Sequence[Observation], -): - # observations have an id and a parent_observation_id. Return a sorted list starting with the root observation where the parent_observation_id is None - parent_to_observation = {obs.parent_observation_id: obs for obs in observations} - - if None not in parent_to_observation: - return [] - - # Start with the root observation (parent_observation_id is None) - current_observation = parent_to_observation[None] - dependencies = [current_observation] - - next_parent_id = current_observation.id - while next_parent_id in parent_to_observation: - current_observation = parent_to_observation[next_parent_id] - dependencies.append(current_observation) - next_parent_id = current_observation.id - - return dependencies - - -def test_observe_dataset_run(): - # Create dataset - langfuse = Langfuse() - dataset_name = create_uuid() - langfuse.create_dataset(name=dataset_name) - - items_data = [] - num_items = 3 - - for i in range(num_items): - trace_id = langfuse.create_trace_id() - dataset_item_input = "Hello World " + str(i) - langfuse.create_dataset_item( - dataset_name=dataset_name, input=dataset_item_input - ) - - items_data.append((dataset_item_input, trace_id)) - - dataset = langfuse.get_dataset(dataset_name) - assert len(dataset.items) == num_items - - run_name = create_uuid() - - @observe() - def run_llm_app_on_dataset_item(input): - return input - - def wrapperFunc(input): - return run_llm_app_on_dataset_item(input) - - def execute_dataset_item(item, run_name): - with item.run(run_name=run_name) as span: - trace_id = span.trace_id - span.update_trace( - name="run_llm_app_on_dataset_item", - input={"args": [item.input]}, - output=item.input, - ) - wrapperFunc(item.input) - return trace_id - - # Execute dataset items in parallel - items = dataset.items[::-1] # Reverse order to reflect input order - trace_ids = [] - - with ThreadPoolExecutor() as executor: - for item in items: - result = executor.submit( - execute_dataset_item, - item, - run_name=run_name, - ) - trace_ids.append(result.result()) - - langfuse.flush() - time.sleep(1) # Give API time to process - - # Verify each trace individually - api = get_api() - for i, trace_id in enumerate(trace_ids): - trace = api.trace.get(trace_id) - assert trace is not None - assert trace.name == "run_llm_app_on_dataset_item" - assert trace.output is not None - # Verify the input was properly captured - expected_input = dataset.items[len(dataset.items) - 1 - i].input - assert trace.input is not None - assert "args" in trace.input - assert trace.input["args"][0] == expected_input - assert trace.output == expected_input diff --git a/tests/test_decorators.py b/tests/test_decorators.py deleted file mode 100644 index 47bc3b015..000000000 --- a/tests/test_decorators.py +++ /dev/null @@ -1,1083 +0,0 @@ -import asyncio -from collections import defaultdict -from concurrent.futures import ThreadPoolExecutor -from time import sleep -from typing import Optional - -import pytest -from langchain.prompts import ChatPromptTemplate -from langchain_openai import ChatOpenAI - -from langfuse import get_client, observe -from langfuse.langchain import CallbackHandler -from langfuse.media import LangfuseMedia -from tests.utils import get_api - -mock_metadata = {"key": "metadata"} -mock_deep_metadata = {"key": "mock_deep_metadata"} -mock_session_id = "session-id-1" -mock_args = (1, 2, 3) -mock_kwargs = {"a": 1, "b": 2, "c": 3} - - -def test_nested_observations(): - mock_name = "test_nested_observations" - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - @observe(as_type="generation", name="level_3", capture_output=False) - def level_3_function(): - langfuse.update_current_generation(metadata=mock_metadata) - langfuse.update_current_generation( - metadata=mock_deep_metadata, - usage_details={"input": 150, "output": 50, "total": 300}, - model="gpt-3.5-turbo", - output="mock_output", - ) - langfuse.update_current_generation(version="version-1") - langfuse.update_current_trace(session_id=mock_session_id, name=mock_name) - - langfuse.update_current_trace( - user_id="user_id", - ) - - return "level_3" - - @observe(name="level_2_manually_set") - def level_2_function(): - level_3_function() - langfuse.update_current_span(metadata=mock_metadata) - - return "level_2" - - @observe() - def level_1_function(*args, **kwargs): - level_2_function() - - return "level_1" - - result = level_1_function( - *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id - ) - langfuse.flush() - - assert result == "level_1" # Wrapped function returns correctly - - # ID setting for span or trace - trace_data = get_api().trace.get(mock_trace_id) - assert len(trace_data.observations) == 3 - - # trace parameters if set anywhere in the call stack - assert trace_data.session_id == mock_session_id - assert trace_data.user_id == "user_id" - assert trace_data.name == mock_name - - # Check correct nesting - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id].append(o) - - assert len(adjacencies) == 3 - - level_1_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - level_2_observation = adjacencies[level_1_observation.id][0] - level_3_observation = adjacencies[level_2_observation.id][0] - - assert level_1_observation.name == "level_1_function" - assert level_1_observation.input == {"args": list(mock_args), "kwargs": mock_kwargs} - assert level_1_observation.output == "level_1" - - assert level_2_observation.name == "level_2_manually_set" - assert level_2_observation.metadata["key"] == mock_metadata["key"] - - assert level_3_observation.name == "level_3" - assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] - assert level_3_observation.type == "GENERATION" - assert level_3_observation.calculated_total_cost > 0 - assert level_3_observation.output == "mock_output" - assert level_3_observation.version == "version-1" - - -def test_nested_observations_with_non_parentheses_decorator(): - mock_name = "test_nested_observations" - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - @observe(as_type="generation", name="level_3", capture_output=False) - def level_3_function(): - langfuse.update_current_generation(metadata=mock_metadata) - langfuse.update_current_generation( - metadata=mock_deep_metadata, - usage_details={"input": 150, "output": 50, "total": 300}, - model="gpt-3.5-turbo", - output="mock_output", - ) - langfuse.update_current_generation(version="version-1") - - langfuse.update_current_trace(session_id=mock_session_id, name=mock_name) - - langfuse.update_current_trace( - user_id="user_id", - ) - - return "level_3" - - @observe - def level_2_function(): - level_3_function() - langfuse.update_current_span(metadata=mock_metadata) - - return "level_2" - - @observe - def level_1_function(*args, **kwargs): - level_2_function() - - return "level_1" - - result = level_1_function( - *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id - ) - langfuse.flush() - - assert result == "level_1" # Wrapped function returns correctly - - # ID setting for span or trace - trace_data = get_api().trace.get(mock_trace_id) - assert len(trace_data.observations) == 3 - - # trace parameters if set anywhere in the call stack - assert trace_data.session_id == mock_session_id - assert trace_data.user_id == "user_id" - assert trace_data.name == mock_name - - # Check correct nesting - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id or o.trace_id].append(o) - - assert len(adjacencies) == 3 - - level_1_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - level_2_observation = adjacencies[level_1_observation.id][0] - level_3_observation = adjacencies[level_2_observation.id][0] - - assert level_1_observation.name == "level_1_function" - assert level_1_observation.input == {"args": list(mock_args), "kwargs": mock_kwargs} - assert level_1_observation.output == "level_1" - - assert level_2_observation.name == "level_2_function" - assert level_2_observation.metadata["key"] == mock_metadata["key"] - - assert level_3_observation.name == "level_3" - assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] - assert level_3_observation.type == "GENERATION" - assert level_3_observation.calculated_total_cost > 0 - assert level_3_observation.output == "mock_output" - assert level_3_observation.version == "version-1" - - -# behavior on exceptions -def test_exception_in_wrapped_function(): - mock_name = "test_exception_in_wrapped_function" - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - @observe(as_type="generation", capture_output=False) - def level_3_function(): - langfuse.update_current_generation(metadata=mock_metadata) - langfuse.update_current_generation( - metadata=mock_deep_metadata, - usage_details={"input": 150, "output": 50, "total": 300}, - model="gpt-3.5-turbo", - ) - langfuse.update_current_trace(session_id=mock_session_id, name=mock_name) - - raise ValueError("Mock exception") - - @observe() - def level_2_function(): - level_3_function() - langfuse.update_current_generation(metadata=mock_metadata) - - return "level_2" - - @observe() - def level_1_function(*args, **kwargs): - sleep(1) - level_2_function() - print("hello") - - return "level_1" - - # Check that the exception is raised - with pytest.raises(ValueError): - level_1_function(*mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id) - - langfuse.flush() - - trace_data = get_api().trace.get(mock_trace_id) - - # trace parameters if set anywhere in the call stack - assert trace_data.session_id == mock_session_id - assert trace_data.name == mock_name - - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id or o.trace_id].append(o) - - assert len(adjacencies) == 3 - - level_1_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - level_2_observation = adjacencies[level_1_observation.id][0] - level_3_observation = adjacencies[level_2_observation.id][0] - - assert level_1_observation.name == "level_1_function" - assert level_1_observation.input == {"args": list(mock_args), "kwargs": mock_kwargs} - - assert level_2_observation.name == "level_2_function" - - assert level_3_observation.name == "level_3_function" - assert level_3_observation.type == "GENERATION" - - assert level_3_observation.status_message == "Mock exception" - assert level_3_observation.level == "ERROR" - - -# behavior on concurrency -def test_concurrent_decorator_executions(): - mock_name = "test_concurrent_decorator_executions" - langfuse = get_client() - mock_trace_id_1 = langfuse.create_trace_id() - mock_trace_id_2 = langfuse.create_trace_id() - - @observe(as_type="generation", capture_output=False) - def level_3_function(): - langfuse.update_current_generation(metadata=mock_metadata) - langfuse.update_current_generation(metadata=mock_deep_metadata) - langfuse.update_current_generation( - metadata=mock_deep_metadata, - usage_details={"input": 150, "output": 50, "total": 300}, - model="gpt-3.5-turbo", - ) - langfuse.update_current_trace(name=mock_name, session_id=mock_session_id) - - return "level_3" - - @observe() - def level_2_function(): - level_3_function() - langfuse.update_current_generation(metadata=mock_metadata) - - return "level_2" - - @observe(name=mock_name) - def level_1_function(*args, **kwargs): - sleep(1) - level_2_function() - - return "level_1" - - with ThreadPoolExecutor(max_workers=2) as executor: - future1 = executor.submit( - level_1_function, - *mock_args, - mock_trace_id_1, - **mock_kwargs, - langfuse_trace_id=mock_trace_id_1, - ) - future2 = executor.submit( - level_1_function, - *mock_args, - mock_trace_id_2, - **mock_kwargs, - langfuse_trace_id=mock_trace_id_2, - ) - - future1.result() - future2.result() - - langfuse.flush() - - for mock_id in [mock_trace_id_1, mock_trace_id_2]: - trace_data = get_api().trace.get(mock_id) - assert len(trace_data.observations) == 3 - - # ID setting for span or trace - assert trace_data.session_id == mock_session_id - assert trace_data.name == mock_name - - # Check correct nesting - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id].append(o) - - assert len(adjacencies) == 3 - - level_1_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - level_2_observation = adjacencies[level_1_observation.id][0] - level_3_observation = adjacencies[level_2_observation.id][0] - - assert level_1_observation.name == mock_name - assert level_1_observation.input == { - "args": list(mock_args) + [mock_id], - "kwargs": mock_kwargs, - } - assert level_1_observation.output == "level_1" - - assert level_2_observation.metadata["key"] == mock_metadata["key"] - - assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] - assert level_3_observation.type == "GENERATION" - assert level_3_observation.calculated_total_cost > 0 - - -def test_decorators_langchain(): - mock_name = "test_decorators_langchain" - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - @observe() - def langchain_operations(*args, **kwargs): - # Get langfuse callback handler for LangChain - handler = CallbackHandler() - prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}") - model = ChatOpenAI(temperature=0) - - chain = prompt | model - - return chain.invoke( - {"topic": kwargs["topic"]}, - config={ - "callbacks": [handler], - }, - ) - - @observe() - def level_3_function(*args, **kwargs): - langfuse.update_current_span(metadata=mock_metadata) - langfuse.update_current_span(metadata=mock_deep_metadata) - langfuse.update_current_trace(session_id=mock_session_id, name=mock_name) - - return langchain_operations(*args, **kwargs) - - @observe() - def level_2_function(*args, **kwargs): - langfuse.update_current_span(metadata=mock_metadata) - - return level_3_function(*args, **kwargs) - - @observe() - def level_1_function(*args, **kwargs): - return level_2_function(*args, **kwargs) - - level_1_function(topic="socks", langfuse_trace_id=mock_trace_id) - - langfuse.flush() - - trace_data = get_api().trace.get(mock_trace_id) - assert len(trace_data.observations) > 2 - - # Check correct nesting - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id].append(o) - - assert len(adjacencies) > 2 - - # trace parameters if set anywhere in the call stack - assert trace_data.session_id == mock_session_id - assert trace_data.name == mock_name - - # Check that the langchain_operations is at the correct level - level_1_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - level_2_observation = adjacencies[level_1_observation.id][0] - level_3_observation = adjacencies[level_2_observation.id][0] - langchain_observation = adjacencies[level_3_observation.id][0] - - assert level_1_observation.name == "level_1_function" - assert level_2_observation.name == "level_2_function" - assert level_2_observation.metadata["key"] == mock_metadata["key"] - assert level_3_observation.name == "level_3_function" - assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] - assert langchain_observation.name == "langchain_operations" - - # Check that LangChain components are captured - assert any([o.name == "ChatPromptTemplate" for o in trace_data.observations]) - - -def test_get_current_trace_url(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - @observe() - def level_3_function(): - return langfuse.get_trace_url(trace_id=langfuse.get_current_trace_id()) - - @observe() - def level_2_function(): - return level_3_function() - - @observe() - def level_1_function(*args, **kwargs): - return level_2_function() - - result = level_1_function( - *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id - ) - langfuse.flush() - - expected_url = f"http://localhost:3000/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a/traces/{mock_trace_id}" - assert result == expected_url - - -def test_scoring_observations(): - mock_name = "test_scoring_observations" - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - @observe(as_type="generation", capture_output=False) - def level_3_function(): - langfuse.score_current_span(name="test-observation-score", value=1) - langfuse.score_current_trace(name="another-test-trace-score", value="my_value") - - return "level_3" - - @observe() - def level_2_function(): - return level_3_function() - - @observe() - def level_1_function(*args, **kwargs): - langfuse.score_current_trace(name="test-trace-score", value=3) - langfuse.update_current_trace(name=mock_name) - return level_2_function() - - result = level_1_function( - *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id - ) - langfuse.flush() - sleep(1) - - assert result == "level_3" # Wrapped function returns correctly - - # ID setting for span or trace - trace_data = get_api().trace.get(mock_trace_id) - assert ( - len(trace_data.observations) == 3 - ) # Top-most function is trace, so it's not an observations - assert trace_data.name == mock_name - - # Check for correct scoring - scores = trace_data.scores - - assert len(scores) == 3 - - trace_scores = [ - s for s in scores if s.trace_id == mock_trace_id and s.observation_id is None - ] - observation_score = [s for s in scores if s.observation_id is not None][0] - - assert any( - [ - score.name == "another-test-trace-score" - and score.string_value == "my_value" - and score.data_type == "CATEGORICAL" - for score in trace_scores - ] - ) - assert any( - [ - score.name == "test-trace-score" - and score.value == 3 - and score.data_type == "NUMERIC" - for score in trace_scores - ] - ) - - assert observation_score.name == "test-observation-score" - assert observation_score.value == 1 - assert observation_score.data_type == "NUMERIC" - - -def test_circular_reference_handling(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - # Define a class that will contain a circular reference - class CircularRefObject: - def __init__(self): - self.reference: Optional[CircularRefObject] = None - - @observe() - def function_with_circular_arg(circular_obj, *args, **kwargs): - # This function doesn't need to do anything with circular_obj, - # the test is simply to see if it can be called without error. - return "function response" - - # Create an instance of the object and establish a circular reference - circular_obj = CircularRefObject() - circular_obj.reference = circular_obj - - # Call the decorated function, passing the circularly-referenced object - result = function_with_circular_arg(circular_obj, langfuse_trace_id=mock_trace_id) - - langfuse.flush() - - # Validate that the function executed as expected - assert result == "function response" - - trace_data = get_api().trace.get(mock_trace_id) - - assert ( - trace_data.observations[0].input["args"][0]["reference"] == "CircularRefObject" - ) - - -def test_disabled_io_capture(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - class Node: - def __init__(self, value: tuple): - self.value = value - - @observe(capture_input=False, capture_output=False) - def nested(*args, **kwargs): - langfuse.update_current_span( - input=Node(("manually set tuple", 1)), output="manually set output" - ) - return "nested response" - - @observe(capture_output=False) - def main(*args, **kwargs): - nested(*args, **kwargs) - return "function response" - - result = main("Hello, World!", name="John", langfuse_trace_id=mock_trace_id) - - langfuse.flush() - - assert result == "function response" - - trace_data = get_api().trace.get(mock_trace_id) - - # Check that disabled capture_io doesn't capture manually set input/output - assert len(trace_data.observations) == 2 - # Only one of the observations must satisfy this - found_match = False - for observation in trace_data.observations: - if ( - observation.input - and isinstance(observation.input, dict) - and "value" in observation.input - and observation.input["value"] == ["manually set tuple", 1] - and observation.output == "manually set output" - ): - found_match = True - break - assert found_match, "No observation found with expected input and output" - - -def test_decorated_class_and_instance_methods(): - mock_name = "test_decorated_class_and_instance_methods" - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - class TestClass: - @classmethod - @observe(name="class-method") - def class_method(cls, *args, **kwargs): - langfuse.update_current_span() - return "class_method" - - @observe(as_type="generation", capture_output=False) - def level_3_function(self): - langfuse.update_current_generation(metadata=mock_metadata) - langfuse.update_current_generation( - metadata=mock_deep_metadata, - usage_details={"input": 150, "output": 50, "total": 300}, - model="gpt-3.5-turbo", - output="mock_output", - ) - - langfuse.update_current_trace(session_id=mock_session_id, name=mock_name) - - return "level_3" - - @observe() - def level_2_function(self): - TestClass.class_method() - - self.level_3_function() - langfuse.update_current_span(metadata=mock_metadata) - - return "level_2" - - @observe() - def level_1_function(self, *args, **kwargs): - self.level_2_function() - - return "level_1" - - result = TestClass().level_1_function( - *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id - ) - - langfuse.flush() - - assert result == "level_1" # Wrapped function returns correctly - - # ID setting for span or trace - trace_data = get_api().trace.get(mock_trace_id) - assert len(trace_data.observations) == 4 - - # trace parameters if set anywhere in the call stack - assert trace_data.session_id == mock_session_id - assert trace_data.name == mock_name - - # Check correct nesting - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id].append(o) - - assert len(adjacencies) == 3 - - level_1_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - level_2_observation = adjacencies[level_1_observation.id][0] - - # Find level_3_observation and class_method_observation in level_2's children - level_2_children = adjacencies[level_2_observation.id] - level_3_observation = next(o for o in level_2_children if o.name != "class-method") - class_method_observation = next( - o for o in level_2_children if o.name == "class-method" - ) - - assert level_1_observation.name == "level_1_function" - assert level_1_observation.input == {"args": list(mock_args), "kwargs": mock_kwargs} - assert level_1_observation.output == "level_1" - - assert level_2_observation.name == "level_2_function" - assert level_2_observation.metadata["key"] == mock_metadata["key"] - - assert class_method_observation.name == "class-method" - assert class_method_observation.output == "class_method" - - assert level_3_observation.name == "level_3_function" - assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] - assert level_3_observation.type == "GENERATION" - assert level_3_observation.calculated_total_cost > 0 - assert level_3_observation.output == "mock_output" - - -def test_generator_as_return_value(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - mock_output = "Hello, World!" - - def custom_transform_to_string(x): - return "--".join(x) - - def generator_function(): - yield "Hello" - yield ", " - yield "World!" - - @observe(transform_to_string=custom_transform_to_string) - def nested(): - return generator_function() - - @observe() - def main(**kwargs): - gen = nested() - - result = "" - for item in gen: - result += item - - return result - - result = main(langfuse_trace_id=mock_trace_id) - langfuse.flush() - - assert result == mock_output - - trace_data = get_api().trace.get(mock_trace_id) - - # Find the main and nested observations - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id].append(o) - - main_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - nested_observation = adjacencies[main_observation.id][0] - - assert main_observation.name == "main" - assert main_observation.output == mock_output - - assert nested_observation.name == "nested" - assert nested_observation.output == "Hello--, --World!" - - -@pytest.mark.asyncio -async def test_async_generator_as_return_value(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - mock_output = "Hello, async World!" - - def custom_transform_to_string(x): - return "--".join(x) - - @observe(transform_to_string=custom_transform_to_string) - async def async_generator_function(): - await asyncio.sleep(0.1) # Simulate async operation - yield "Hello" - await asyncio.sleep(0.1) - yield ", async " - await asyncio.sleep(0.1) - yield "World!" - - @observe() - async def main_async(**kwargs): - gen = async_generator_function() - - result = "" - async for item in gen: - result += item - - return result - - result = await main_async(langfuse_trace_id=mock_trace_id) - langfuse.flush() - - assert result == mock_output - - trace_data = get_api().trace.get(mock_trace_id) - - # Check correct nesting - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id].append(o) - - main_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - nested_observation = adjacencies[main_observation.id][0] - - assert main_observation.name == "main_async" - assert main_observation.output == mock_output - - assert nested_observation.name == "async_generator_function" - assert nested_observation.output == "Hello--, async --World!" - - -@pytest.mark.asyncio -async def test_async_nested_openai_chat_stream(): - from langfuse.openai import AsyncOpenAI - - mock_name = "test_async_nested_openai_chat_stream" - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - mock_tags = ["tag1", "tag2"] - mock_session_id = "session-id-1" - mock_user_id = "user-id-1" - - @observe(capture_output=False) - async def level_2_function(): - gen = await AsyncOpenAI().chat.completions.create( - model="gpt-3.5-turbo", - messages=[{"role": "user", "content": "1 + 1 = "}], - temperature=0, - metadata={"someKey": "someResponse"}, - stream=True, - ) - - langfuse.update_current_trace( - session_id=mock_session_id, - user_id=mock_user_id, - tags=mock_tags, - ) - - async for c in gen: - print(c) - - langfuse.update_current_span(metadata=mock_metadata) - langfuse.update_current_trace(name=mock_name) - - return "level_2" - - @observe() - async def level_1_function(*args, **kwargs): - await level_2_function() - - return "level_1" - - result = await level_1_function( - *mock_args, **mock_kwargs, langfuse_trace_id=mock_trace_id - ) - langfuse.flush() - - assert result == "level_1" # Wrapped function returns correctly - - # ID setting for span or trace - trace_data = get_api().trace.get(mock_trace_id) - assert len(trace_data.observations) == 3 - - # trace parameters if set anywhere in the call stack - assert trace_data.session_id == mock_session_id - assert trace_data.name == mock_name - - # Check correct nesting - adjacencies = defaultdict(list) - for o in trace_data.observations: - adjacencies[o.parent_observation_id or o.trace_id].append(o) - - assert len(adjacencies) == 3 - - level_1_observation = next( - o - for o in trace_data.observations - if o.parent_observation_id not in [o.id for o in trace_data.observations] - ) - level_2_observation = adjacencies[level_1_observation.id][0] - level_3_observation = adjacencies[level_2_observation.id][0] - - assert level_2_observation.metadata["key"] == mock_metadata["key"] - - generation = level_3_observation - - assert generation.name == "OpenAI-generation" - assert generation.metadata["someKey"] == "someResponse" - assert generation.input == [{"content": "1 + 1 = ", "role": "user"}] - assert generation.type == "GENERATION" - assert "gpt-3.5-turbo" in generation.model - assert generation.start_time is not None - assert generation.end_time is not None - assert generation.start_time < generation.end_time - assert generation.model_parameters == { - "temperature": 0, - "top_p": 1, - "frequency_penalty": 0, - "max_tokens": "Infinity", - "presence_penalty": 0, - } - assert generation.usage.input is not None - assert generation.usage.output is not None - assert generation.usage.total is not None - print(generation) - assert generation.output == "2" - - -def test_generator_as_function_input(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - mock_output = "Hello, World!" - - def generator_function(): - yield "Hello" - yield ", " - yield "World!" - - @observe() - def nested(gen): - result = "" - for item in gen: - result += item - - return result - - @observe() - def main(**kwargs): - gen = generator_function() - - return nested(gen) - - result = main(langfuse_trace_id=mock_trace_id) - langfuse.flush() - - assert result == mock_output - - trace_data = get_api().trace.get(mock_trace_id) - - nested_obs = next(o for o in trace_data.observations if o.name == "nested") - - assert nested_obs.input["args"][0] == "" - assert nested_obs.output == "Hello, World!" - - observation_start_time = nested_obs.start_time - observation_end_time = nested_obs.end_time - - assert observation_start_time is not None - assert observation_end_time is not None - assert observation_start_time <= observation_end_time - - -def test_nest_list_of_generator_as_function_IO(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - def generator_function(): - yield "Hello" - yield ", " - yield "World!" - - @observe() - def nested(list_of_gens): - return list_of_gens - - @observe() - def main(**kwargs): - gen = generator_function() - - return nested([(gen, gen)]) - - main(langfuse_trace_id=mock_trace_id) - langfuse.flush() - - trace_data = get_api().trace.get(mock_trace_id) - - # Find the observation with name 'nested' - nested_observation = next(o for o in trace_data.observations if o.name == "nested") - - assert [[["", ""]]] == nested_observation.input["args"] - - assert all( - ["generator" in arg for arg in nested_observation.output[0]], - ) - - observation_start_time = nested_observation.start_time - observation_end_time = nested_observation.end_time - - assert observation_start_time is not None - assert observation_end_time is not None - assert observation_start_time <= observation_end_time - - -def test_return_dict_for_output(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - mock_output = {"key": "value"} - - @observe() - def function(): - return mock_output - - result = function(langfuse_trace_id=mock_trace_id) - langfuse.flush() - - assert result == mock_output - - trace_data = get_api().trace.get(mock_trace_id) - assert trace_data.observations[0].output == mock_output - - -def test_media(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - with open("static/bitcoin.pdf", "rb") as pdf_file: - pdf_bytes = pdf_file.read() - - media = LangfuseMedia(content_bytes=pdf_bytes, content_type="application/pdf") - - @observe() - def main(): - sleep(1) - langfuse.update_current_trace( - input={ - "context": { - "nested": media, - }, - }, - output={ - "context": { - "nested": media, - }, - }, - metadata={ - "context": { - "nested": media, - }, - }, - ) - - main(langfuse_trace_id=mock_trace_id) - - langfuse.flush() - - trace_data = get_api().trace.get(mock_trace_id) - - assert ( - "@@@langfuseMedia:type=application/pdf|id=" - in trace_data.input["context"]["nested"] - ) - assert ( - "@@@langfuseMedia:type=application/pdf|id=" - in trace_data.output["context"]["nested"] - ) - assert ( - "@@@langfuseMedia:type=application/pdf|id=" - in trace_data.metadata["context"]["nested"] - ) - parsed_reference_string = LangfuseMedia.parse_reference_string( - trace_data.metadata["context"]["nested"] - ) - assert parsed_reference_string["content_type"] == "application/pdf" - assert parsed_reference_string["media_id"] is not None - assert parsed_reference_string["source"] == "bytes" - - -def test_merge_metadata_and_tags(): - langfuse = get_client() - mock_trace_id = langfuse.create_trace_id() - - @observe - def nested(): - langfuse.update_current_trace(metadata={"key2": "value2"}, tags=["tag2"]) - - @observe - def main(): - langfuse.update_current_trace(metadata={"key1": "value1"}, tags=["tag1"]) - - nested() - - main(langfuse_trace_id=mock_trace_id) - - langfuse.flush() - - trace_data = get_api().trace.get(mock_trace_id) - - assert trace_data.metadata["key1"] == "value1" - assert trace_data.metadata["key2"] == "value2" - - assert trace_data.tags == ["tag1", "tag2"] diff --git a/tests/test_extract_model.py b/tests/test_extract_model.py deleted file mode 100644 index 5db2961f6..000000000 --- a/tests/test_extract_model.py +++ /dev/null @@ -1,158 +0,0 @@ -from typing import Any -from unittest.mock import MagicMock - -import pytest -from langchain.schema.messages import HumanMessage -from langchain_anthropic import Anthropic, ChatAnthropic -from langchain_aws import BedrockLLM, ChatBedrock -from langchain_community.chat_models import ( - ChatCohere, - ChatTongyi, -) -from langchain_community.chat_models.fake import FakeMessagesListChatModel - -# from langchain_huggingface.llms import HuggingFacePipeline -from langchain_community.llms.textgen import TextGen -from langchain_core.load.dump import default -from langchain_google_vertexai import ChatVertexAI -from langchain_groq import ChatGroq -from langchain_mistralai.chat_models import ChatMistralAI -from langchain_ollama import ChatOllama, OllamaLLM -from langchain_openai import ( - AzureChatOpenAI, - ChatOpenAI, - OpenAI, -) - -from langfuse.langchain import CallbackHandler -from langfuse.langchain.utils import _extract_model_name -from tests.utils import get_api - - -@pytest.mark.parametrize( - "expected_model,model", - [ - ( - "mixtral-8x7b-32768", - ChatGroq( - temperature=0, model_name="mixtral-8x7b-32768", groq_api_key="something" - ), - ), - ("llama3", OllamaLLM(model="llama3")), - ("llama3", ChatOllama(model="llama3")), - ( - None, - FakeMessagesListChatModel(responses=[HumanMessage("Hello, how are you?")]), - ), - ( - "mistralai", - ChatMistralAI(mistral_api_key="mistral_api_key", model="mistralai"), - ), - ( - "text-gen", - TextGen(model_url="some-url"), - ), # local deployments, does not have a model name - ("claude-2", ChatAnthropic(model_name="claude-2")), - ( - "claude-3-sonnet-20240229", - ChatAnthropic(model="claude-3-sonnet-20240229"), - ), - ("claude-2", Anthropic()), - ("claude-2", Anthropic()), - ("command", ChatCohere(model="command", cohere_api_key="command")), - (None, ChatTongyi(dashscope_api_key="dash")), - ( - "amazon.titan-tg1-large", - BedrockLLM( - model="amazon.titan-tg1-large", - region="us-east-1", - client=MagicMock(), - ), - ), - ( - "anthropic.claude-3-sonnet-20240229-v1:0", - ChatBedrock( - model_id="anthropic.claude-3-sonnet-20240229-v1:0", - region_name="us-east-1", - client=MagicMock(), - ), - ), - ( - "claude-1", - BedrockLLM( - model="claude-1", - region="us-east-1", - client=MagicMock(), - ), - ), - ], -) -def test_models(expected_model: str, model: Any): - serialized = default(model) - model_name = _extract_model_name(serialized) - assert model_name == expected_model - - -# all models here need to be tested here because we take the model from the kwargs / invocation_params or we need to make an actual call for setup -@pytest.mark.skip("Flaky") -@pytest.mark.parametrize( - "expected_model,model", - [ - ("gpt-3.5-turbo-0125", ChatOpenAI()), - ("gpt-3.5-turbo-instruct", OpenAI()), - ( - "gpt-3.5-turbo", - AzureChatOpenAI( - openai_api_version="2023-05-15", - model="gpt-3.5-turbo", - azure_deployment="your-deployment-name", - azure_endpoint="https://your-endpoint-name.azurewebsites.net", - ), - ), - # ( - # "gpt2", - # HuggingFacePipeline( - # model_id="gpt2", - # model_kwargs={ - # "max_new_tokens": 512, - # "top_k": 30, - # "temperature": 0.1, - # "repetition_penalty": 1.03, - # }, - # ), - # ), - ( - "qwen-72b-chat", - ChatTongyi(model="qwen-72b-chat", dashscope_api_key="dashscope"), - ), - ( - "gemini", - ChatVertexAI( - model_name="gemini", credentials=MagicMock(), project="some-project" - ), - ), - ], -) -def test_entire_llm_call(expected_model, model): - callback = CallbackHandler() - - with callback.client.start_as_current_span(name="parent") as span: - trace_id = span.trace_id - - try: - # LLM calls are failing, because of missing API keys etc. - # However, we are still able to extract the model names beforehand. - model.invoke("Hello, how are you?", config={"callbacks": [callback]}) - except Exception as e: - print(e) - pass - - callback.client.flush() - api = get_api() - - trace = api.trace.get(trace_id) - - assert len(trace.observations) == 2 - - generation = list(filter(lambda o: o.type == "GENERATION", trace.observations))[0] - assert generation.model == expected_model diff --git a/tests/test_prompt.py b/tests/test_prompt.py deleted file mode 100644 index d3c20d285..000000000 --- a/tests/test_prompt.py +++ /dev/null @@ -1,1412 +0,0 @@ -from time import sleep -from unittest.mock import Mock, patch - -import openai -import pytest - -from langfuse._client.client import Langfuse -from langfuse._utils.prompt_cache import ( - DEFAULT_PROMPT_CACHE_TTL_SECONDS, - PromptCacheItem, -) -from langfuse.api.resources.prompts import Prompt_Chat, Prompt_Text -from langfuse.model import ChatPromptClient, TextPromptClient -from tests.utils import create_uuid, get_api - - -def test_create_prompt(): - langfuse = Langfuse() - prompt_name = create_uuid() - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt="test prompt", - labels=["production"], - commit_message="initial commit", - ) - - second_prompt_client = langfuse.get_prompt(prompt_name) - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.config == second_prompt_client.config - assert prompt_client.commit_message == second_prompt_client.commit_message - assert prompt_client.config == {} - - -def test_create_prompt_with_special_chars_in_name(): - langfuse = Langfuse() - prompt_name = create_uuid() + "special chars !@#$%^&*() +" - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt="test prompt", - labels=["production"], - tags=["test"], - ) - - second_prompt_client = langfuse.get_prompt(prompt_name) - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.tags == second_prompt_client.tags - assert prompt_client.config == second_prompt_client.config - assert prompt_client.config == {} - - -def test_create_chat_prompt(): - langfuse = Langfuse() - prompt_name = create_uuid() - - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt=[ - {"role": "system", "content": "test prompt 1 with {{animal}}"}, - {"role": "user", "content": "test prompt 2 with {{occupation}}"}, - ], - labels=["production"], - tags=["test"], - type="chat", - commit_message="initial commit", - ) - - second_prompt_client = langfuse.get_prompt(prompt_name, type="chat") - - # Create a test generation - completion = openai.OpenAI().chat.completions.create( - model="gpt-4", - messages=prompt_client.compile(animal="dog", occupation="doctor"), - ) - - assert len(completion.choices) > 0 - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.config == second_prompt_client.config - assert prompt_client.labels == ["production", "latest"] - assert prompt_client.tags == second_prompt_client.tags - assert prompt_client.commit_message == second_prompt_client.commit_message - assert prompt_client.config == {} - - -def test_create_chat_prompt_with_placeholders(): - langfuse = Langfuse() - prompt_name = create_uuid() - - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt=[ - {"role": "system", "content": "You are a {{role}} assistant"}, - {"type": "placeholder", "name": "history"}, - {"role": "user", "content": "Help me with {{task}}"}, - ], - labels=["production"], - tags=["test"], - type="chat", - commit_message="initial commit", - ) - - second_prompt_client = langfuse.get_prompt(prompt_name, type="chat") - messages = second_prompt_client.compile( - role="helpful", - task="coding", - history=[ - {"role": "user", "content": "Example: {{task}}"}, - {"role": "assistant", "content": "Example response"}, - ], - ) - - # Create a test generation using compiled messages - completion = openai.OpenAI().chat.completions.create( - model="gpt-4", - messages=messages, - ) - - assert len(completion.choices) > 0 - assert len(messages) == 4 - assert messages[0]["content"] == "You are a helpful assistant" - assert messages[1]["content"] == "Example: coding" - assert messages[2]["content"] == "Example response" - assert messages[3]["content"] == "Help me with coding" - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.config == second_prompt_client.config - assert prompt_client.labels == ["production", "latest"] - assert prompt_client.tags == second_prompt_client.tags - assert prompt_client.commit_message == second_prompt_client.commit_message - assert prompt_client.config == {} - - -def test_create_prompt_with_placeholders(): - """Test creating a prompt with placeholder messages.""" - langfuse = Langfuse() - prompt_name = create_uuid() - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt=[ - {"role": "system", "content": "System message"}, - {"type": "placeholder", "name": "context"}, - {"role": "user", "content": "User message"}, - ], - type="chat", - ) - - # Verify the full prompt structure with placeholders - assert len(prompt_client.prompt) == 3 - - # First message - system - assert prompt_client.prompt[0]["type"] == "message" - assert prompt_client.prompt[0]["role"] == "system" - assert prompt_client.prompt[0]["content"] == "System message" - # Placeholder - assert prompt_client.prompt[1]["type"] == "placeholder" - assert prompt_client.prompt[1]["name"] == "context" - # Third message - user - assert prompt_client.prompt[2]["type"] == "message" - assert prompt_client.prompt[2]["role"] == "user" - assert prompt_client.prompt[2]["content"] == "User message" - - -def test_get_prompt_with_placeholders(): - """Test retrieving a prompt with placeholders.""" - langfuse = Langfuse() - prompt_name = create_uuid() - - langfuse.create_prompt( - name=prompt_name, - prompt=[ - {"role": "system", "content": "You are {{name}}"}, - {"type": "placeholder", "name": "history"}, - {"role": "user", "content": "{{question}}"}, - ], - type="chat", - ) - - prompt_client = langfuse.get_prompt(prompt_name, type="chat", version=1) - - # Verify placeholder structure is preserved - assert len(prompt_client.prompt) == 3 - - # First message - system with variable - assert prompt_client.prompt[0]["type"] == "message" - assert prompt_client.prompt[0]["role"] == "system" - assert prompt_client.prompt[0]["content"] == "You are {{name}}" - # Placeholder - assert prompt_client.prompt[1]["type"] == "placeholder" - assert prompt_client.prompt[1]["name"] == "history" - # Third message - user with variable - assert prompt_client.prompt[2]["type"] == "message" - assert prompt_client.prompt[2]["role"] == "user" - assert prompt_client.prompt[2]["content"] == "{{question}}" - - -@pytest.mark.parametrize( - ("variables", "placeholders", "expected_len", "expected_contents"), - [ - # 0. Variables only, no placeholders. Unresolved placeholders kept in output - ( - {"role": "helpful", "task": "coding"}, - {}, - 3, - [ - "You are a helpful assistant", - None, - "Help me with coding", - ], # None = placeholder - ), - # 1. No variables, no placeholders. Expect verbatim message+placeholder output - ( - {}, - {}, - 3, - ["You are a {{role}} assistant", None, "Help me with {{task}}"], - ), # None = placeholder - # 2. Placeholders only, empty variables. Expect output with placeholders filled in - ( - {}, - { - "examples": [ - {"role": "user", "content": "Example question"}, - {"role": "assistant", "content": "Example answer"}, - ], - }, - 4, - [ - "You are a {{role}} assistant", - "Example question", - "Example answer", - "Help me with {{task}}", - ], - ), - # 3. Both variables and placeholders. Expect fully compiled output - ( - {"role": "helpful", "task": "coding"}, - { - "examples": [ - {"role": "user", "content": "Show me {{task}}"}, - {"role": "assistant", "content": "Here's {{task}}"}, - ], - }, - 4, - [ - "You are a helpful assistant", - "Show me coding", - "Here's coding", - "Help me with coding", - ], - ), - # # Empty placeholder array - # This is expected to fail! If the user provides a placeholder, it should contain an array - # ( - # {"role": "helpful", "task": "coding"}, - # {"examples": []}, - # 2, - # ["You are a helpful assistant", "Help me with coding"], - # ), - # 4. Unused placeholder fill ins. Unresolved placeholders kept in output - ( - {"role": "helpful", "task": "coding"}, - {"unused": [{"role": "user", "content": "Won't appear"}]}, - 3, - [ - "You are a helpful assistant", - None, - "Help me with coding", - ], # None = placeholder - ), - # 5. Placeholder with non-list value (should log warning and append as string) - ( - {"role": "helpful", "task": "coding"}, - {"examples": "not a list"}, - 3, - [ - "You are a helpful assistant", - "not a list", # String value appended directly - "Help me with coding", - ], - ), - # 6. Placeholder with invalid message structure (should log warning and include both) - ( - {"role": "helpful", "task": "coding"}, - { - "examples": [ - "invalid message", - {"role": "user", "content": "valid message"}, - ] - }, - 4, - [ - "You are a helpful assistant", - "['invalid message', {'role': 'user', 'content': 'valid message'}]", # Invalid structure becomes string - "valid message", # Valid message processed normally - "Help me with coding", - ], - ), - ], -) -def test_compile_with_placeholders( - variables, placeholders, expected_len, expected_contents -) -> None: - """Test compile_with_placeholders with different variable/placeholder combinations.""" - from langfuse.api.resources.prompts import Prompt_Chat - from langfuse.model import ChatPromptClient - - mock_prompt = Prompt_Chat( - name="test_prompt", - version=1, - type="chat", - config={}, - tags=[], - labels=[], - prompt=[ - {"role": "system", "content": "You are a {{role}} assistant"}, - {"type": "placeholder", "name": "examples"}, - {"role": "user", "content": "Help me with {{task}}"}, - ], - ) - - compile_kwargs = {**placeholders, **variables} - result = ChatPromptClient(mock_prompt).compile(**compile_kwargs) - - assert len(result) == expected_len - for i, expected_content in enumerate(expected_contents): - if expected_content is None: - # This should be an unresolved placeholder - assert "type" in result[i] and result[i]["type"] == "placeholder" - elif isinstance(result[i], str): - # This is a string value from invalid placeholder - assert result[i] == expected_content - else: - # This should be a regular message - assert "content" in result[i] - assert result[i]["content"] == expected_content - - -def test_warning_on_unresolved_placeholders(): - """Test that a warning is emitted when compiling with unresolved placeholders.""" - from unittest.mock import patch - - langfuse = Langfuse() - prompt_name = create_uuid() - - langfuse.create_prompt( - name=prompt_name, - prompt=[ - {"role": "system", "content": "You are {{name}}"}, - {"type": "placeholder", "name": "history"}, - {"role": "user", "content": "{{question}}"}, - ], - type="chat", - ) - - prompt_client = langfuse.get_prompt(prompt_name, type="chat", version=1) - - # Test that warning is emitted when compiling with unresolved placeholders - with patch("langfuse.logger.langfuse_logger.warning") as mock_warning: - # Compile without providing the 'history' placeholder - result = prompt_client.compile(name="Assistant", question="What is 2+2?") - - # Verify the warning was called with the expected message - mock_warning.assert_called_once() - warning_message = mock_warning.call_args[0][0] - assert "Placeholders ['history'] have not been resolved" in warning_message - - # Verify the result only contains the resolved messages - assert len(result) == 3 - assert result[0]["content"] == "You are Assistant" - assert result[1]["name"] == "history" - assert result[2]["content"] == "What is 2+2?" - - -def test_compiling_chat_prompt(): - langfuse = Langfuse() - prompt_name = create_uuid() - - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt=[ - { - "role": "system", - "content": "test prompt 1 with {{state}} {{target}} {{state}}", - }, - {"role": "user", "content": "test prompt 2 with {{state}}"}, - ], - labels=["production"], - type="chat", - ) - - second_prompt_client = langfuse.get_prompt(prompt_name, type="chat") - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.labels == ["production", "latest"] - - assert second_prompt_client.compile(target="world", state="great") == [ - {"role": "system", "content": "test prompt 1 with great world great"}, - {"role": "user", "content": "test prompt 2 with great"}, - ] - - -def test_compiling_prompt(): - langfuse = Langfuse() - prompt_name = "test_compiling_prompt" - - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt='Hello, {{target}}! I hope you are {{state}}. {{undefined_variable}}. And here is some JSON that should not be compiled: {{ "key": "value" }} \ - Here is a custom var for users using str.format instead of the mustache-style double curly braces: {custom_var}', - labels=["production"], - ) - - second_prompt_client = langfuse.get_prompt(prompt_name) - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.labels == ["production", "latest"] - - compiled = second_prompt_client.compile(target="world", state="great") - - assert ( - compiled - == 'Hello, world! I hope you are great. {{undefined_variable}}. And here is some JSON that should not be compiled: {{ "key": "value" }} \ - Here is a custom var for users using str.format instead of the mustache-style double curly braces: {custom_var}' - ) - - -def test_compiling_prompt_without_character_escaping(): - langfuse = Langfuse() - prompt_name = "test_compiling_prompt_without_character_escaping" - - prompt_client = langfuse.create_prompt( - name=prompt_name, prompt="Hello, {{ some_json }}", labels=["production"] - ) - - second_prompt_client = langfuse.get_prompt(prompt_name) - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.labels == ["production", "latest"] - - some_json = '{"key": "value"}' - compiled = second_prompt_client.compile(some_json=some_json) - - assert compiled == 'Hello, {"key": "value"}' - - -def test_compiling_prompt_with_content_as_variable_name(): - langfuse = Langfuse() - prompt_name = "test_compiling_prompt_with_content_as_variable_name" - - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt="Hello, {{ content }}!", - labels=["production"], - ) - - second_prompt_client = langfuse.get_prompt(prompt_name) - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.labels == ["production", "latest"] - - compiled = second_prompt_client.compile(content="Jane") - - assert compiled == "Hello, Jane!" - - -def test_create_prompt_with_null_config(): - langfuse = Langfuse(debug=False) - - langfuse.create_prompt( - name="test_null_config", - prompt="Hello, world! I hope you are great", - labels=["production"], - config=None, - ) - - prompt = langfuse.get_prompt("test_null_config") - - assert prompt.config == {} - - -def test_create_prompt_with_tags(): - langfuse = Langfuse(debug=False) - prompt_name = create_uuid() - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - tags=["tag1", "tag2"], - ) - - prompt = langfuse.get_prompt(prompt_name, version=1) - - assert prompt.tags == ["tag1", "tag2"] - - -def test_create_prompt_with_empty_tags(): - langfuse = Langfuse(debug=False) - prompt_name = create_uuid() - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - tags=[], - ) - - prompt = langfuse.get_prompt(prompt_name, version=1) - - assert prompt.tags == [] - - -def test_create_prompt_with_previous_tags(): - langfuse = Langfuse(debug=False) - prompt_name = create_uuid() - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - ) - - prompt = langfuse.get_prompt(prompt_name, version=1) - - assert prompt.tags == [] - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - tags=["tag1", "tag2"], - ) - - prompt_v2 = langfuse.get_prompt(prompt_name, version=2) - - assert prompt_v2.tags == ["tag1", "tag2"] - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - ) - - prompt_v3 = langfuse.get_prompt(prompt_name, version=3) - - assert prompt_v3.tags == ["tag1", "tag2"] - - -def test_remove_prompt_tags(): - langfuse = Langfuse(debug=False) - prompt_name = create_uuid() - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - tags=["tag1", "tag2"], - ) - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - tags=[], - ) - - prompt_v1 = langfuse.get_prompt(prompt_name, version=1) - prompt_v2 = langfuse.get_prompt(prompt_name, version=2) - - assert prompt_v1.tags == [] - assert prompt_v2.tags == [] - - -def test_update_prompt_tags(): - langfuse = Langfuse(debug=False) - prompt_name = create_uuid() - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - tags=["tag1", "tag2"], - ) - - prompt_v1 = langfuse.get_prompt(prompt_name, version=1) - - assert prompt_v1.tags == ["tag1", "tag2"] - - langfuse.create_prompt( - name=prompt_name, - prompt="Hello, world! I hope you are great", - tags=["tag3", "tag4"], - ) - - prompt_v2 = langfuse.get_prompt(prompt_name, version=2) - - assert prompt_v2.tags == ["tag3", "tag4"] - - -def test_get_prompt_by_version_or_label(): - langfuse = Langfuse() - prompt_name = create_uuid() - - for i in range(3): - langfuse.create_prompt( - name=prompt_name, - prompt="test prompt " + str(i + 1), - labels=["production"] if i == 1 else [], - ) - - default_prompt_client = langfuse.get_prompt(prompt_name) - assert default_prompt_client.version == 2 - assert default_prompt_client.prompt == "test prompt 2" - assert default_prompt_client.labels == ["production"] - - first_prompt_client = langfuse.get_prompt(prompt_name, version=1) - assert first_prompt_client.version == 1 - assert first_prompt_client.prompt == "test prompt 1" - assert first_prompt_client.labels == [] - - second_prompt_client = langfuse.get_prompt(prompt_name, version=2) - assert second_prompt_client.version == 2 - assert second_prompt_client.prompt == "test prompt 2" - assert second_prompt_client.labels == ["production"] - - third_prompt_client = langfuse.get_prompt(prompt_name, label="latest") - assert third_prompt_client.version == 3 - assert third_prompt_client.prompt == "test prompt 3" - assert third_prompt_client.labels == ["latest"] - - -def test_prompt_end_to_end(): - langfuse = Langfuse(debug=False) - - langfuse.create_prompt( - name="test", - prompt="Hello, {{target}}! I hope you are {{state}}.", - labels=["production"], - config={"temperature": 0.5}, - ) - - prompt = langfuse.get_prompt("test") - - prompt_str = prompt.compile(target="world", state="great") - assert prompt_str == "Hello, world! I hope you are great." - assert prompt.config == {"temperature": 0.5} - - generation = langfuse.start_generation( - name="mygen", input=prompt_str, prompt=prompt - ).end() - - # to check that these do not error - generation.update(prompt=prompt) - - langfuse.flush() - - api = get_api() - - trace = api.trace.get(generation.trace_id) - - assert len(trace.observations) == 1 - - generation = trace.observations[0] - assert generation.prompt_id is not None - - observation = api.observations.get(generation.id) - - assert observation.prompt_id is not None - - -@pytest.fixture -def langfuse(): - langfuse_instance = Langfuse() - langfuse_instance.api = Mock() - - return langfuse_instance - - -# Fetching a new prompt when nothing in cache -def test_get_fresh_prompt(langfuse): - prompt_name = "test_get_fresh_prompt" - prompt = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - type="text", - labels=[], - config={}, - tags=[], - ) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result = langfuse.get_prompt(prompt_name, fallback="fallback") - mock_server_call.assert_called_once_with( - prompt_name, - version=None, - label=None, - request_options=None, - ) - - assert result == TextPromptClient(prompt) - - -# Should throw an error if prompt name is unspecified -def test_throw_if_name_unspecified(langfuse): - prompt_name = "" - - with pytest.raises(ValueError) as exc_info: - langfuse.get_prompt(prompt_name) - - assert "Prompt name cannot be empty" in str(exc_info.value) - - -# Should throw an error if nothing in cache and fetch fails -def test_throw_when_failing_fetch_and_no_cache(langfuse): - prompt_name = "failing_fetch_and_no_cache" - - mock_server_call = langfuse.api.prompts.get - mock_server_call.side_effect = Exception("Prompt not found") - - with pytest.raises(Exception) as exc_info: - langfuse.get_prompt(prompt_name) - - assert "Prompt not found" in str(exc_info.value) - - -def test_using_custom_prompt_timeouts(langfuse): - prompt_name = "test_using_custom_prompt_timeouts" - prompt = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - type="text", - labels=[], - config={}, - tags=[], - ) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result = langfuse.get_prompt( - prompt_name, fallback="fallback", fetch_timeout_seconds=1000 - ) - mock_server_call.assert_called_once_with( - prompt_name, - version=None, - label=None, - request_options={"timeout_in_seconds": 1000}, - ) - - assert result == TextPromptClient(prompt) - - -# Should throw an error if cache_ttl_seconds is passed as positional rather than keyword argument -def test_throw_if_cache_ttl_seconds_positional_argument(langfuse): - prompt_name = "test ttl seconds in positional arg" - ttl_seconds = 20 - - with pytest.raises(TypeError) as exc_info: - langfuse.get_prompt(prompt_name, ttl_seconds) - - assert "positional arguments" in str(exc_info.value) - - -# Should return cached prompt if not expired -def test_get_valid_cached_prompt(langfuse): - prompt_name = "test_get_valid_cached_prompt" - prompt = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - type="text", - labels=[], - config={}, - tags=[], - ) - prompt_client = TextPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name, fallback="fallback") - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - result_call_2 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 - assert result_call_2 == prompt_client - - -# Should return cached chat prompt if not expired when fetching by label -def test_get_valid_cached_chat_prompt_by_label(langfuse): - prompt_name = "test_get_valid_cached_chat_prompt_by_label" - prompt = Prompt_Chat( - name=prompt_name, - version=1, - prompt=[{"role": "system", "content": "Make me laugh"}], - labels=["test"], - type="chat", - config={}, - tags=[], - ) - prompt_client = ChatPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name, label="test") - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - result_call_2 = langfuse.get_prompt(prompt_name, label="test") - assert mock_server_call.call_count == 1 - assert result_call_2 == prompt_client - - -# Should return cached chat prompt if not expired when fetching by version -def test_get_valid_cached_chat_prompt_by_version(langfuse): - prompt_name = "test_get_valid_cached_chat_prompt_by_version" - prompt = Prompt_Chat( - name=prompt_name, - version=1, - prompt=[{"role": "system", "content": "Make me laugh"}], - labels=["test"], - type="chat", - config={}, - tags=[], - ) - prompt_client = ChatPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name, version=1) - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - result_call_2 = langfuse.get_prompt(prompt_name, version=1) - assert mock_server_call.call_count == 1 - assert result_call_2 == prompt_client - - -# Should return cached chat prompt if fetching the default prompt or the 'production' labeled one -def test_get_valid_cached_production_chat_prompt(langfuse): - prompt_name = "test_get_valid_cached_production_chat_prompt" - prompt = Prompt_Chat( - name=prompt_name, - version=1, - prompt=[{"role": "system", "content": "Make me laugh"}], - labels=["test"], - type="chat", - config={}, - tags=[], - ) - prompt_client = ChatPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - result_call_2 = langfuse.get_prompt(prompt_name, label="production") - assert mock_server_call.call_count == 1 - assert result_call_2 == prompt_client - - -# Should return cached chat prompt if not expired -def test_get_valid_cached_chat_prompt(langfuse): - prompt_name = "test_get_valid_cached_chat_prompt" - prompt = Prompt_Chat( - name=prompt_name, - version=1, - prompt=[{"role": "system", "content": "Make me laugh"}], - labels=[], - type="chat", - config={}, - tags=[], - ) - prompt_client = ChatPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - result_call_2 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 - assert result_call_2 == prompt_client - - -# Should refetch and return new prompt if cached one is expired according to custom TTL -@patch.object(PromptCacheItem, "get_epoch_seconds") -def test_get_fresh_prompt_when_expired_cache_custom_ttl(mock_time, langfuse: Langfuse): - mock_time.return_value = 0 - ttl_seconds = 20 - - prompt_name = "test_get_fresh_prompt_when_expired_cache_custom_ttl" - prompt = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - config={"temperature": 0.9}, - labels=[], - type="text", - tags=[], - ) - prompt_client = TextPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name, cache_ttl_seconds=ttl_seconds) - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - # Set time to just BEFORE cache expiry - mock_time.return_value = ttl_seconds - 1 - - result_call_2 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 # No new call - assert result_call_2 == prompt_client - - # Set time to just AFTER cache expiry - mock_time.return_value = ttl_seconds + 1 - - result_call_3 = langfuse.get_prompt(prompt_name) - - while True: - if langfuse._resources.prompt_cache._task_manager.active_tasks() == 0: - break - sleep(0.1) - - assert mock_server_call.call_count == 2 # New call - assert result_call_3 == prompt_client - - -# Should disable caching when cache_ttl_seconds is set to 0 -@patch.object(PromptCacheItem, "get_epoch_seconds") -def test_disable_caching_when_ttl_zero(mock_time, langfuse: Langfuse): - mock_time.return_value = 0 - prompt_name = "test_disable_caching_when_ttl_zero" - - # Initial prompt - prompt1 = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - labels=[], - type="text", - config={}, - tags=[], - ) - - # Updated prompts - prompt2 = Prompt_Text( - name=prompt_name, - version=2, - prompt="Tell me a joke", - labels=[], - type="text", - config={}, - tags=[], - ) - prompt3 = Prompt_Text( - name=prompt_name, - version=3, - prompt="Share a funny story", - labels=[], - type="text", - config={}, - tags=[], - ) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.side_effect = [prompt1, prompt2, prompt3] - - # First call - result1 = langfuse.get_prompt(prompt_name, cache_ttl_seconds=0) - assert mock_server_call.call_count == 1 - assert result1 == TextPromptClient(prompt1) - - # Second call - result2 = langfuse.get_prompt(prompt_name, cache_ttl_seconds=0) - assert mock_server_call.call_count == 2 - assert result2 == TextPromptClient(prompt2) - - # Third call - result3 = langfuse.get_prompt(prompt_name, cache_ttl_seconds=0) - assert mock_server_call.call_count == 3 - assert result3 == TextPromptClient(prompt3) - - # Verify that all results are different - assert result1 != result2 != result3 - - -# Should return stale prompt immediately if cached one is expired according to default TTL and add to refresh promise map -@patch.object(PromptCacheItem, "get_epoch_seconds") -def test_get_stale_prompt_when_expired_cache_default_ttl(mock_time, langfuse: Langfuse): - import logging - - logging.basicConfig(level=logging.DEBUG) - mock_time.return_value = 0 - - prompt_name = "test_get_stale_prompt_when_expired_cache_default_ttl" - prompt = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - labels=[], - type="text", - config={}, - tags=[], - ) - prompt_client = TextPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - # Update the version of the returned mocked prompt - updated_prompt = Prompt_Text( - name=prompt_name, - version=2, - prompt="Make me laugh", - labels=[], - type="text", - config={}, - tags=[], - ) - mock_server_call.return_value = updated_prompt - - # Set time to just AFTER cache expiry - mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1 - - stale_result = langfuse.get_prompt(prompt_name) - assert stale_result == prompt_client - - # Ensure that only one refresh is triggered despite multiple calls - # Cannot check for value as the prompt might have already been updated - langfuse.get_prompt(prompt_name) - langfuse.get_prompt(prompt_name) - langfuse.get_prompt(prompt_name) - langfuse.get_prompt(prompt_name) - - while True: - if langfuse._resources.prompt_cache._task_manager.active_tasks() == 0: - break - sleep(0.1) - - assert mock_server_call.call_count == 2 # Only one new call to server - - # Check that the prompt has been updated after refresh - updated_result = langfuse.get_prompt(prompt_name) - assert updated_result.version == 2 - assert updated_result == TextPromptClient(updated_prompt) - - -# Should refetch and return new prompt if cached one is expired according to default TTL -@patch.object(PromptCacheItem, "get_epoch_seconds") -def test_get_fresh_prompt_when_expired_cache_default_ttl(mock_time, langfuse: Langfuse): - mock_time.return_value = 0 - - prompt_name = "test_get_fresh_prompt_when_expired_cache_default_ttl" - prompt = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - labels=[], - type="text", - config={}, - tags=[], - ) - prompt_client = TextPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - # Set time to just BEFORE cache expiry - mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS - 1 - - result_call_2 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 # No new call - assert result_call_2 == prompt_client - - # Set time to just AFTER cache expiry - mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1 - - result_call_3 = langfuse.get_prompt(prompt_name) - while True: - if langfuse._resources.prompt_cache._task_manager.active_tasks() == 0: - break - sleep(0.1) - - assert mock_server_call.call_count == 2 # New call - assert result_call_3 == prompt_client - - -# Should return expired prompt if refetch fails -@patch.object(PromptCacheItem, "get_epoch_seconds") -def test_get_expired_prompt_when_failing_fetch(mock_time, langfuse: Langfuse): - mock_time.return_value = 0 - - prompt_name = "test_get_expired_prompt_when_failing_fetch" - prompt = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - labels=[], - type="text", - config={}, - tags=[], - ) - prompt_client = TextPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name) - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - # Set time to just AFTER cache expiry - mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1 - - mock_server_call.side_effect = Exception("Server error") - - result_call_2 = langfuse.get_prompt(prompt_name, max_retries=1) - while True: - if langfuse._resources.prompt_cache._task_manager.active_tasks() == 0: - break - sleep(0.1) - - assert mock_server_call.call_count == 3 - assert result_call_2 == prompt_client - - -# Should fetch new prompt if version changes -def test_get_fresh_prompt_when_version_changes(langfuse: Langfuse): - prompt_name = "test_get_fresh_prompt_when_version_changes" - prompt = Prompt_Text( - name=prompt_name, - version=1, - prompt="Make me laugh", - labels=[], - type="text", - config={}, - tags=[], - ) - prompt_client = TextPromptClient(prompt) - - mock_server_call = langfuse.api.prompts.get - mock_server_call.return_value = prompt - - result_call_1 = langfuse.get_prompt(prompt_name, version=1) - assert mock_server_call.call_count == 1 - assert result_call_1 == prompt_client - - version_changed_prompt = Prompt_Text( - name=prompt_name, - version=2, - labels=[], - prompt="Make me laugh", - type="text", - config={}, - tags=[], - ) - version_changed_prompt_client = TextPromptClient(version_changed_prompt) - mock_server_call.return_value = version_changed_prompt - - result_call_2 = langfuse.get_prompt(prompt_name, version=2) - assert mock_server_call.call_count == 2 - assert result_call_2 == version_changed_prompt_client - - -def test_do_not_return_fallback_if_fetch_success(): - langfuse = Langfuse() - prompt_name = create_uuid() - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt="test prompt", - labels=["production"], - ) - - second_prompt_client = langfuse.get_prompt(prompt_name, fallback="fallback") - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.config == second_prompt_client.config - assert prompt_client.config == {} - - -def test_fallback_text_prompt(): - langfuse = Langfuse() - - fallback_text_prompt = "this is a fallback text prompt with {{variable}}" - - # Should throw an error if prompt not found and no fallback provided - with pytest.raises(Exception): - langfuse.get_prompt("nonexistent_prompt") - - prompt = langfuse.get_prompt("nonexistent_prompt", fallback=fallback_text_prompt) - - assert prompt.prompt == fallback_text_prompt - assert ( - prompt.compile(variable="value") == "this is a fallback text prompt with value" - ) - - -def test_fallback_chat_prompt(): - langfuse = Langfuse() - fallback_chat_prompt = [ - {"role": "system", "content": "fallback system"}, - {"role": "user", "content": "fallback user name {{name}}"}, - ] - - # Should throw an error if prompt not found and no fallback provided - with pytest.raises(Exception): - langfuse.get_prompt("nonexistent_chat_prompt", type="chat") - - prompt = langfuse.get_prompt( - "nonexistent_chat_prompt", type="chat", fallback=fallback_chat_prompt - ) - - # Check that the prompt structure contains the fallback data (allowing for internal formatting) - assert len(prompt.prompt) == len(fallback_chat_prompt) - assert all(msg["type"] == "message" for msg in prompt.prompt) - assert prompt.prompt[0]["role"] == "system" - assert prompt.prompt[0]["content"] == "fallback system" - assert prompt.prompt[1]["role"] == "user" - assert prompt.prompt[1]["content"] == "fallback user name {{name}}" - assert prompt.compile(name="Jane") == [ - {"role": "system", "content": "fallback system"}, - {"role": "user", "content": "fallback user name Jane"}, - ] - - -def test_do_not_link_observation_if_fallback(): - langfuse = Langfuse() - - fallback_text_prompt = "this is a fallback text prompt with {{variable}}" - - # Should throw an error if prompt not found and no fallback provided - with pytest.raises(Exception): - langfuse.get_prompt("nonexistent_prompt") - - prompt = langfuse.get_prompt("nonexistent_prompt", fallback=fallback_text_prompt) - - generation = langfuse.start_generation( - name="mygen", prompt=prompt, input="this is a test input" - ).end() - langfuse.flush() - - api = get_api() - trace = api.trace.get(generation.trace_id) - - assert len(trace.observations) == 1 - assert trace.observations[0].prompt_id is None - - -def test_variable_names_on_content_with_variable_names(): - langfuse = Langfuse() - - prompt_client = langfuse.create_prompt( - name="test_variable_names_1", - prompt="test prompt with var names {{ var1 }} {{ var2 }}", - labels=["production"], - type="text", - ) - - second_prompt_client = langfuse.get_prompt("test_variable_names_1") - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.labels == ["production", "latest"] - - var_names = second_prompt_client.variables - - assert var_names == ["var1", "var2"] - - -def test_variable_names_on_content_with_no_variable_names(): - langfuse = Langfuse() - - prompt_client = langfuse.create_prompt( - name="test_variable_names_2", - prompt="test prompt with no var names", - labels=["production"], - type="text", - ) - - second_prompt_client = langfuse.get_prompt("test_variable_names_2") - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.labels == ["production", "latest"] - - var_names = second_prompt_client.variables - - assert var_names == [] - - -def test_variable_names_on_content_with_variable_names_chat_messages(): - langfuse = Langfuse() - - prompt_client = langfuse.create_prompt( - name="test_variable_names_3", - prompt=[ - { - "role": "system", - "content": "test prompt with template vars {{ var1 }} {{ var2 }}", - }, - {"role": "user", "content": "test prompt 2 with template vars {{ var3 }}"}, - ], - labels=["production"], - type="chat", - ) - - second_prompt_client = langfuse.get_prompt("test_variable_names_3") - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.labels == ["production", "latest"] - - var_names = second_prompt_client.variables - - assert var_names == ["var1", "var2", "var3"] - - -def test_variable_names_on_content_with_no_variable_names_chat_messages(): - langfuse = Langfuse() - prompt_name = "test_variable_names_on_content_with_no_variable_names_chat_messages" - - prompt_client = langfuse.create_prompt( - name=prompt_name, - prompt=[ - {"role": "system", "content": "test prompt with no template vars"}, - {"role": "user", "content": "test prompt 2 with no template vars"}, - ], - labels=["production"], - type="chat", - ) - - second_prompt_client = langfuse.get_prompt(prompt_name) - - assert prompt_client.name == second_prompt_client.name - assert prompt_client.version == second_prompt_client.version - assert prompt_client.prompt == second_prompt_client.prompt - assert prompt_client.labels == ["production", "latest"] - - var_names = second_prompt_client.variables - - assert var_names == [] - - -def test_update_prompt(): - langfuse = Langfuse() - prompt_name = create_uuid() - - # Create initial prompt - langfuse.create_prompt( - name=prompt_name, - prompt="test prompt", - labels=["production"], - ) - - # Update prompt labels - updated_prompt = langfuse.update_prompt( - name=prompt_name, - version=1, - new_labels=["john", "doe"], - ) - - # Fetch prompt after update (should be invalidated) - fetched_prompt = langfuse.get_prompt(prompt_name) - - # Verify the fetched prompt matches the updated values - assert fetched_prompt.name == prompt_name - assert fetched_prompt.version == 1 - print(f"Fetched prompt labels: {fetched_prompt.labels}") - print(f"Updated prompt labels: {updated_prompt.labels}") - - # production was set by the first call, latest is managed and set by Langfuse - expected_labels = sorted(["latest", "doe", "production", "john"]) - assert sorted(fetched_prompt.labels) == expected_labels - assert sorted(updated_prompt.labels) == expected_labels diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_additional_headers_simple.py b/tests/unit/test_additional_headers_simple.py similarity index 78% rename from tests/test_additional_headers_simple.py rename to tests/unit/test_additional_headers_simple.py index 4298a3ba5..dd843b35a 100644 --- a/tests/test_additional_headers_simple.py +++ b/tests/unit/test_additional_headers_simple.py @@ -3,11 +3,25 @@ This module tests that additional headers are properly configured in the HTTP clients. """ +from typing import Sequence + import httpx +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from langfuse._client.client import Langfuse +class NoOpSpanExporter(SpanExporter): + """Minimal exporter used to verify custom exporter injection.""" + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + return SpanExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + class TestAdditionalHeadersSimple: """Simple test suite for additional_headers functionality.""" @@ -100,6 +114,21 @@ def test_custom_httpx_client_without_additional_headers_preserves_client(self): == "existing-value" ) + def test_media_manager_uses_custom_httpx_client(self): + """Test that media manager reuses the configured custom httpx client.""" + custom_client = httpx.Client() + + langfuse = Langfuse( + public_key="test-public-key", + secret_key="test-secret-key", + host="https://mock-host.com", + httpx_client=custom_client, + tracing_enabled=False, + ) + + assert langfuse._resources is not None + assert langfuse._resources._media_manager._httpx_client is custom_client + def test_none_additional_headers_works(self): """Test that passing None for additional_headers works without errors.""" langfuse = Langfuse( @@ -143,7 +172,7 @@ def test_span_processor_has_additional_headers_in_otel_exporter(self): processor = LangfuseSpanProcessor( public_key="test-public-key", secret_key="test-secret-key", - host="https://mock-host.com", + base_url="https://mock-host.com", additional_headers=additional_headers, ) @@ -156,8 +185,8 @@ def test_span_processor_has_additional_headers_in_otel_exporter(self): # Verify default headers are still present assert "Authorization" in exporter._headers - assert "x_langfuse_sdk_name" in exporter._headers - assert "x_langfuse_public_key" in exporter._headers + assert "x-langfuse-sdk-name" in exporter._headers + assert "x-langfuse-public-key" in exporter._headers # Check that our override worked assert exporter._headers["X-Override-Default"] == "override-value" @@ -170,7 +199,7 @@ def test_span_processor_none_additional_headers_works(self): processor = LangfuseSpanProcessor( public_key="test-public-key", secret_key="test-secret-key", - host="https://mock-host.com", + base_url="https://mock-host.com", additional_headers=None, ) @@ -179,5 +208,21 @@ def test_span_processor_none_additional_headers_works(self): # Verify default headers are present assert "Authorization" in exporter._headers - assert "x_langfuse_sdk_name" in exporter._headers - assert "x_langfuse_public_key" in exporter._headers + assert "x-langfuse-sdk-name" in exporter._headers + assert "x-langfuse-public-key" in exporter._headers + + def test_span_processor_uses_custom_span_exporter_when_provided(self): + """Test that a custom exporter bypasses the default OTLP exporter construction.""" + from langfuse._client.span_processor import LangfuseSpanProcessor + + custom_exporter = NoOpSpanExporter() + + processor = LangfuseSpanProcessor( + public_key="test-public-key", + secret_key="test-secret-key", + base_url="https://mock-host.com", + additional_headers={"X-Custom-Trace-Header": "trace-value"}, + span_exporter=custom_exporter, + ) + + assert processor.span_exporter is custom_exporter diff --git a/tests/unit/test_app_root_detection.py b/tests/unit/test_app_root_detection.py new file mode 100644 index 000000000..334237c5d --- /dev/null +++ b/tests/unit/test_app_root_detection.py @@ -0,0 +1,507 @@ +import threading +from concurrent.futures import ThreadPoolExecutor + +from opentelemetry import baggage +from opentelemetry import context as otel_context_api +from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags + +from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse._client.constants import LANGFUSE_TRACER_NAME +from langfuse._client.propagation import ( + LANGFUSE_TRACE_ID_BAGGAGE_KEY, + _get_langfuse_trace_id_from_baggage, + _set_langfuse_trace_id_in_baggage, +) +from langfuse._client.span_processor import LangfuseSpanProcessor + +PUBLIC_KEY = "test-public-key" +SECRET_KEY = "test-secret-key" + + +def _create_processor(memory_exporter, **kwargs): + tracer_provider = TracerProvider() + processor = LangfuseSpanProcessor( + public_key=PUBLIC_KEY, + secret_key=SECRET_KEY, + base_url="http://test-host", + span_exporter=memory_exporter, + **kwargs, + ) + tracer_provider.add_span_processor(processor) + + return tracer_provider, processor + + +def _get_spans_by_name(memory_exporter): + return {span.name: span for span in memory_exporter.get_finished_spans()} + + +def _langfuse_tracer(tracer_provider): + return tracer_provider.get_tracer( + LANGFUSE_TRACER_NAME, + "test", + attributes={"public_key": PUBLIC_KEY}, + ) + + +def test_filtered_parent_marks_exported_children_as_app_roots(memory_exporter): + tracer_provider, processor = _create_processor(memory_exporter) + filtered_tracer = tracer_provider.get_tracer("requests") + langfuse_tracer = _langfuse_tracer(tracer_provider) + + with filtered_tracer.start_as_current_span("filtered-parent"): + with langfuse_tracer.start_as_current_span("child-a"): + pass + + with langfuse_tracer.start_as_current_span("child-b"): + pass + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert "filtered-parent" not in spans + assert spans["child-a"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert spans["child-b"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert processor._span_export_expectation_by_id == {} + + +def test_exported_parent_suppresses_exported_child_app_root(memory_exporter): + tracer_provider, processor = _create_processor(memory_exporter) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + with langfuse_tracer.start_as_current_span("parent"): + with langfuse_tracer.start_as_current_span("child"): + pass + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert spans["parent"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert LangfuseOtelSpanAttributes.IS_APP_ROOT not in spans["child"].attributes + assert processor._span_export_expectation_by_id == {} + + +def test_grandparent_baggage_claim_suppresses_child_through_filtered_parent( + memory_exporter, +): + tracer_provider, processor = _create_processor(memory_exporter) + filtered_tracer = tracer_provider.get_tracer("requests") + langfuse_tracer = _langfuse_tracer(tracer_provider) + + with langfuse_tracer.start_as_current_span("grandparent") as grandparent: + context_with_claim = baggage.set_baggage( + name=LANGFUSE_TRACE_ID_BAGGAGE_KEY, + value=format(grandparent.context.trace_id, "032x"), + context=otel_context_api.get_current(), + ) + token = otel_context_api.attach(context_with_claim) + try: + with filtered_tracer.start_as_current_span("filtered-parent"): + with langfuse_tracer.start_as_current_span("child"): + pass + finally: + otel_context_api.detach(token) + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert "filtered-parent" not in spans + assert ( + spans["grandparent"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + ) + assert LangfuseOtelSpanAttributes.IS_APP_ROOT not in spans["child"].attributes + assert processor._span_export_expectation_by_id == {} + + +def test_same_trace_baggage_claim_suppresses_local_app_root(memory_exporter): + tracer_provider, processor = _create_processor(memory_exporter) + langfuse_tracer = _langfuse_tracer(tracer_provider) + trace_id = int("1" * 32, 16) + parent_context = _remote_parent_context(trace_id=trace_id) + parent_context = baggage.set_baggage( + name=LANGFUSE_TRACE_ID_BAGGAGE_KEY, + value=format(trace_id, "032x"), + context=parent_context, + ) + + span = langfuse_tracer.start_span("downstream-root", context=parent_context) + span.end() + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert ( + LangfuseOtelSpanAttributes.IS_APP_ROOT + not in spans["downstream-root"].attributes + ) + assert processor._span_export_expectation_by_id == {} + + +def test_different_trace_baggage_claim_does_not_suppress_local_app_root( + memory_exporter, +): + tracer_provider, processor = _create_processor(memory_exporter) + langfuse_tracer = _langfuse_tracer(tracer_provider) + trace_id = int("1" * 32, 16) + parent_context = _remote_parent_context(trace_id=trace_id) + parent_context = baggage.set_baggage( + name=LANGFUSE_TRACE_ID_BAGGAGE_KEY, + value="2" * 32, + context=parent_context, + ) + + span = langfuse_tracer.start_span("downstream-root", context=parent_context) + span.end() + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert ( + spans["downstream-root"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] + is True + ) + assert processor._span_export_expectation_by_id == {} + + +def test_local_baggage_claim_suppresses_child_even_when_parent_is_filtered( + memory_exporter, +): + def should_export_span(span: ReadableSpan) -> bool: + return span.name != "parent" + + tracer_provider, processor = _create_processor( + memory_exporter, + should_export_span=should_export_span, + ) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + with langfuse_tracer.start_as_current_span("parent") as parent: + context_with_claim = baggage.set_baggage( + name=LANGFUSE_TRACE_ID_BAGGAGE_KEY, + value=format(parent.context.trace_id, "032x"), + context=otel_context_api.get_current(), + ) + token = otel_context_api.attach(context_with_claim) + + try: + with langfuse_tracer.start_as_current_span("child"): + pass + finally: + otel_context_api.detach(token) + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert "parent" not in spans + assert LangfuseOtelSpanAttributes.IS_APP_ROOT not in spans["child"].attributes + assert processor._span_export_expectation_by_id == {} + + +def test_start_time_false_positive_can_leave_exported_child_without_app_root( + memory_exporter, +): + def should_export_span(span: ReadableSpan) -> bool: + if span.name == "parent": + return span.end_time is None + + return True + + tracer_provider, processor = _create_processor( + memory_exporter, + should_export_span=should_export_span, + ) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + with langfuse_tracer.start_as_current_span("parent"): + with langfuse_tracer.start_as_current_span("child"): + pass + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert "parent" not in spans + assert LangfuseOtelSpanAttributes.IS_APP_ROOT not in spans["child"].attributes + assert processor._span_export_expectation_by_id == {} + + +def test_active_langfuse_scope_sets_baggage_after_root_start( + langfuse_memory_client, + memory_exporter, +): + with langfuse_memory_client.start_as_current_observation(name="root") as root: + baggage_entries = baggage.get_all(context=otel_context_api.get_current()) + + assert baggage_entries[LANGFUSE_TRACE_ID_BAGGAGE_KEY] == root.trace_id + + with langfuse_memory_client.start_as_current_observation(name="child"): + pass + + langfuse_memory_client.flush() + + spans = _get_spans_by_name(memory_exporter) + + assert spans["root"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert LangfuseOtelSpanAttributes.IS_APP_ROOT not in spans["child"].attributes + assert "langfuse.trace.metadata.trace_id" not in spans["child"].attributes + + +def test_blocked_instrumentation_scope_parent_marks_child_as_app_root( + memory_exporter, +): + tracer_provider, processor = _create_processor( + memory_exporter, + blocked_instrumentation_scopes=["blocked.scope"], + ) + blocked_tracer = tracer_provider.get_tracer("blocked.scope") + langfuse_tracer = _langfuse_tracer(tracer_provider) + + with blocked_tracer.start_as_current_span("blocked-parent"): + with langfuse_tracer.start_as_current_span("child"): + pass + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert "blocked-parent" not in spans + assert spans["child"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert processor._span_export_expectation_by_id == {} + + +def test_foreign_project_langfuse_parent_marks_child_as_app_root(memory_exporter): + tracer_provider, processor = _create_processor(memory_exporter) + foreign_tracer = tracer_provider.get_tracer( + LANGFUSE_TRACER_NAME, + "test", + attributes={"public_key": "different-public-key"}, + ) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + with foreign_tracer.start_as_current_span("foreign-parent"): + with langfuse_tracer.start_as_current_span("child"): + pass + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert "foreign-parent" not in spans + assert spans["child"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert processor._span_export_expectation_by_id == {} + + +def test_should_export_span_raising_does_not_mark_app_root(memory_exporter): + def should_export_span(span: ReadableSpan) -> bool: + raise RuntimeError("boom") + + tracer_provider, processor = _create_processor( + memory_exporter, + should_export_span=should_export_span, + ) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + with langfuse_tracer.start_as_current_span("root"): + pass + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert "root" not in spans + assert processor._span_export_expectation_by_id == {} + + +def test_mark_app_root_candidate_exception_is_swallowed(memory_exporter, monkeypatch): + tracer_provider, processor = _create_processor(memory_exporter) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + def raise_boom(*args, **kwargs): + raise RuntimeError("boom") + + monkeypatch.setattr(processor, "_mark_app_root_candidate", raise_boom) + + with langfuse_tracer.start_as_current_span("root"): + pass + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert "root" in spans + assert LangfuseOtelSpanAttributes.IS_APP_ROOT not in spans["root"].attributes + + +def test_concurrent_traces_keep_state_consistent(memory_exporter): + tracer_provider, processor = _create_processor(memory_exporter) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + thread_count = 16 + spans_per_thread = 25 + barrier = threading.Barrier(thread_count) + + def worker(worker_id: int) -> None: + barrier.wait() + for span_index in range(spans_per_thread): + with langfuse_tracer.start_as_current_span( + f"root-{worker_id}-{span_index}" + ): + with langfuse_tracer.start_as_current_span( + f"child-{worker_id}-{span_index}" + ): + pass + + with ThreadPoolExecutor(max_workers=thread_count) as pool: + list(pool.map(worker, range(thread_count))) + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + expected_total = thread_count * spans_per_thread + root_spans = [span for name, span in spans.items() if name.startswith("root-")] + child_spans = [span for name, span in spans.items() if name.startswith("child-")] + + assert len(root_spans) == expected_total + assert len(child_spans) == expected_total + assert all( + span.attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + for span in root_spans + ) + assert all( + LangfuseOtelSpanAttributes.IS_APP_ROOT not in span.attributes + for span in child_spans + ) + assert processor._span_export_expectation_by_id == {} + + +def test_multiple_interleaved_traces_track_active_span_state_independently( + memory_exporter, +): + tracer_provider, processor = _create_processor(memory_exporter) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + trace_a_root = langfuse_tracer.start_span("trace-a-root") + trace_a_ctx = trace_api.set_span_in_context(trace_a_root) + trace_b_root = langfuse_tracer.start_span("trace-b-root") + trace_b_ctx = trace_api.set_span_in_context(trace_b_root) + + assert len(processor._span_export_expectation_by_id) == 2 + + trace_a_child = langfuse_tracer.start_span("trace-a-child", context=trace_a_ctx) + trace_b_child = langfuse_tracer.start_span("trace-b-child", context=trace_b_ctx) + + assert len(processor._span_export_expectation_by_id) == 4 + + trace_b_child.end() + trace_b_root.end() + + assert len(processor._span_export_expectation_by_id) == 2 + + trace_a_child.end() + trace_a_root.end() + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert ( + spans["trace-a-root"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + ) + assert ( + spans["trace-b-root"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + ) + assert ( + LangfuseOtelSpanAttributes.IS_APP_ROOT not in spans["trace-a-child"].attributes + ) + assert ( + LangfuseOtelSpanAttributes.IS_APP_ROOT not in spans["trace-b-child"].attributes + ) + assert processor._span_export_expectation_by_id == {} + + +def test_child_started_after_parent_end_is_marked_as_app_root(memory_exporter): + tracer_provider, processor = _create_processor(memory_exporter) + langfuse_tracer = _langfuse_tracer(tracer_provider) + + parent = langfuse_tracer.start_span("parent") + parent_ctx = trace_api.set_span_in_context(parent) + + parent.end() + + child = langfuse_tracer.start_span("child", context=parent_ctx) + child.end() + + processor.force_flush() + + spans = _get_spans_by_name(memory_exporter) + + assert spans["parent"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert spans["child"].attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert processor._span_export_expectation_by_id == {} + + +def test_set_langfuse_trace_id_in_baggage_sets_value(): + trace_id = "a" * 32 + context = _set_langfuse_trace_id_in_baggage( + trace_id=trace_id, + context=otel_context_api.Context(), + ) + + assert _get_langfuse_trace_id_from_baggage(context) == trace_id + + +def test_set_langfuse_trace_id_in_baggage_normalizes_case(): + context = _set_langfuse_trace_id_in_baggage( + trace_id="ABCDEF" + "0" * 26, + context=otel_context_api.Context(), + ) + + assert _get_langfuse_trace_id_from_baggage(context) == "abcdef" + "0" * 26 + + +def test_set_langfuse_trace_id_in_baggage_is_idempotent_for_same_trace(): + trace_id = "a" * 32 + context = _set_langfuse_trace_id_in_baggage( + trace_id=trace_id, + context=otel_context_api.Context(), + ) + + same_context = _set_langfuse_trace_id_in_baggage( + trace_id=trace_id, + context=context, + ) + + assert same_context is context + + +def test_set_langfuse_trace_id_in_baggage_overwrites_for_different_trace(): + first = _set_langfuse_trace_id_in_baggage( + trace_id="a" * 32, + context=otel_context_api.Context(), + ) + + second = _set_langfuse_trace_id_in_baggage( + trace_id="b" * 32, + context=first, + ) + + assert second is not first + assert _get_langfuse_trace_id_from_baggage(second) == "b" * 32 + + +def _remote_parent_context(*, trace_id: int): + span_context = SpanContext( + trace_id=trace_id, + span_id=int("a" * 16, 16), + is_remote=True, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + + return trace_api.set_span_in_context(NonRecordingSpan(span_context)) diff --git a/tests/unit/test_e2e_sharding.py b/tests/unit/test_e2e_sharding.py new file mode 100644 index 000000000..a1ec84e58 --- /dev/null +++ b/tests/unit/test_e2e_sharding.py @@ -0,0 +1,54 @@ +import importlib.util +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT_PATH = REPO_ROOT / "scripts" / "select_e2e_shard.py" + + +def load_shard_script(): + spec = importlib.util.spec_from_file_location("select_e2e_shard", SCRIPT_PATH) + if spec is None or spec.loader is None: + raise AssertionError(f"Unable to load shard selector from {SCRIPT_PATH}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_e2e_shards_cover_all_files_once(): + shard_script = load_shard_script() + + all_files = sorted( + path.relative_to(REPO_ROOT).as_posix() + for path in (REPO_ROOT / "tests" / "e2e").glob("test_*.py") + ) + + shards, shard_loads = shard_script.assign_shards( + shard_script.discover_e2e_files(), shard_count=2 + ) + + assert len(shards) == 2 + assert set(shards[0]).isdisjoint(shards[1]) + assert sorted([path for shard in shards for path in shard]) == all_files + assert all(load > 0 for load in shard_loads) + + +def test_unknown_file_weight_falls_back_to_test_count(tmp_path: Path): + shard_script = load_shard_script() + + test_file = tmp_path / "test_future_suite.py" + test_file.write_text( + "\n".join( + [ + "def test_one():", + " pass", + "", + "async def test_two():", + " pass", + ] + ), + encoding="utf-8", + ) + + assert shard_script.count_test_functions(test_file) == 2 + assert shard_script.estimate_weight(test_file) == 2 diff --git a/tests/unit/test_e2e_support.py b/tests/unit/test_e2e_support.py new file mode 100644 index 000000000..8320bd2fe --- /dev/null +++ b/tests/unit/test_e2e_support.py @@ -0,0 +1,173 @@ +from types import SimpleNamespace + +from langfuse.api.commons.errors.not_found_error import NotFoundError +from tests.support.api_wrapper import LangfuseAPI as SupportLangfuseAPI +from tests.support.retry import retry_until_ready +from tests.support.utils import get_api, wait_for_trace + + +def test_get_api_retries_not_found(monkeypatch): + monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) + + attempts = {"count": 0} + + class FakeTraceService: + def get(self, trace_id): + attempts["count"] += 1 + + if attempts["count"] < 3: + raise NotFoundError( + body={ + "error": "LangfuseNotFoundError", + "message": f"Trace {trace_id} not found within authorized project", + } + ) + + return {"id": trace_id} + + class FakeClient: + trace = FakeTraceService() + + monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: FakeClient()) + + trace = get_api().trace.get("trace-123") + + assert trace == {"id": "trace-123"} + assert attempts["count"] == 3 + + +def test_get_api_retries_filtered_lists(monkeypatch): + monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) + + attempts = {"count": 0} + + class FakeTraceService: + def list(self, **kwargs): + attempts["count"] += 1 + + if attempts["count"] < 3: + return SimpleNamespace(data=[]) + + return SimpleNamespace(data=[kwargs["name"]]) + + class FakeClient: + trace = FakeTraceService() + + monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: FakeClient()) + + response = get_api().trace.list(name="ready-trace") + + assert response.data == ["ready-trace"] + assert attempts["count"] == 3 + + +def test_get_api_retry_can_be_disabled(monkeypatch): + attempts = {"count": 0} + + class FakeTraceService: + def list(self, **kwargs): + attempts["count"] += 1 + return SimpleNamespace(data=[]) + + class FakeClient: + trace = FakeTraceService() + + monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: FakeClient()) + + response = get_api(retry=False).trace.list(name="missing-trace") + + assert response.data == [] + assert attempts["count"] == 1 + + +def test_raw_api_wrapper_retries_not_found_payload(monkeypatch): + monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) + + attempts = {"count": 0} + + class FakeResponse: + def __init__(self, status_code, payload): + self.status_code = status_code + self._payload = payload + self.headers = {} + + def json(self): + return self._payload + + def fake_get(*args, **kwargs): + attempts["count"] += 1 + + if attempts["count"] < 3: + return FakeResponse( + 404, + { + "error": "LangfuseNotFoundError", + "message": "Trace trace-123 not found within authorized project", + }, + ) + + return FakeResponse(200, {"id": "trace-123", "observations": []}) + + monkeypatch.setattr("tests.support.api_wrapper.httpx.get", fake_get) + + api = SupportLangfuseAPI(username="user", password="pass", base_url="http://test") + trace = api.get_trace("trace-123") + + assert trace["id"] == "trace-123" + assert attempts["count"] == 3 + + +def test_wait_for_trace_retries_until_predicate_matches(monkeypatch): + monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) + + attempts = {"count": 0} + + class FakeTraceService: + def get(self, trace_id): + attempts["count"] += 1 + return {"id": trace_id, "observations": [1] * attempts["count"]} + + class FakeClient: + trace = FakeTraceService() + + monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: FakeClient()) + + trace = wait_for_trace( + "trace-123", is_result_ready=lambda trace: len(trace["observations"]) == 3 + ) + + assert trace["id"] == "trace-123" + assert len(trace["observations"]) == 3 + assert attempts["count"] == 3 + + +def test_retry_until_ready_clears_stale_error_after_success(monkeypatch): + monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) + + monotonic_values = iter([0.0, 0.0, 0.05, 0.06, 0.11, 0.11]) + monkeypatch.setattr("tests.support.retry.monotonic", lambda: next(monotonic_values)) + + attempts = {"count": 0} + + def operation(): + attempts["count"] += 1 + + if attempts["count"] == 1: + raise NotFoundError( + body={ + "error": "LangfuseNotFoundError", + "message": "Trace trace-123 not found within authorized project", + } + ) + + return {"id": "trace-123", "attempt": attempts["count"], "observations": []} + + trace = retry_until_ready( + operation, + is_result_ready=lambda _: False, + timeout_seconds=0.1, + interval_seconds=0, + ) + + assert trace["id"] == "trace-123" + assert trace["attempt"] == 3 diff --git a/tests/test_error_logging.py b/tests/unit/test_error_logging.py similarity index 99% rename from tests/test_error_logging.py rename to tests/unit/test_error_logging.py index 637a8de98..fd5224d7a 100644 --- a/tests/test_error_logging.py +++ b/tests/unit/test_error_logging.py @@ -1,9 +1,10 @@ import logging + import pytest from langfuse._utils.error_logging import ( - catch_and_log_errors, auto_decorate_methods_with, + catch_and_log_errors, ) diff --git a/tests/test_error_parsing.py b/tests/unit/test_error_parsing.py similarity index 96% rename from tests/test_error_parsing.py rename to tests/unit/test_error_parsing.py index db53f3d4d..36a7decbf 100644 --- a/tests/test_error_parsing.py +++ b/tests/unit/test_error_parsing.py @@ -5,14 +5,14 @@ generate_error_message_fern, ) from langfuse._utils.request import APIError, APIErrors -from langfuse.api.core import ApiError -from langfuse.api.resources.commons.errors import ( +from langfuse.api import ( AccessDeniedError, MethodNotAllowedError, NotFoundError, + ServiceUnavailableError, UnauthorizedError, ) -from langfuse.api.resources.health.errors import ServiceUnavailableError +from langfuse.api.core import ApiError def test_generate_error_message_api_error(): diff --git a/tests/unit/test_experiment.py b/tests/unit/test_experiment.py new file mode 100644 index 000000000..c6c8465a3 --- /dev/null +++ b/tests/unit/test_experiment.py @@ -0,0 +1,248 @@ +"""Tests for ``langfuse.experiment`` — ``RunnerContext`` and ``RegressionError``.""" + +import inspect +import typing +from datetime import datetime +from typing import get_type_hints +from unittest.mock import MagicMock + +import pytest + +from langfuse import RegressionError, RunnerContext +from langfuse._client.client import Langfuse +from langfuse.batch_evaluation import CompositeEvaluatorFunction + + +def _noop_task(*, item, **kwargs): # pragma: no cover - never invoked via mock + return None + + +def _make_ctx(**kwargs) -> RunnerContext: + client = MagicMock(spec=Langfuse) + client.run_experiment.return_value = "result-sentinel" + return RunnerContext(client=client, **kwargs) + + +class TestRunnerContextDefaults: + def test_context_defaults_flow_through(self): + ctx_data = [{"input": "a"}] + ctx_version = datetime(2026, 1, 1) + ctx = _make_ctx( + data=ctx_data, + dataset_version=ctx_version, + metadata={"sha": "abc123"}, + ) + + result = ctx.run_experiment(name="exp", task=_noop_task) + + assert result == "result-sentinel" + ctx.client.run_experiment.assert_called_once() + kwargs = ctx.client.run_experiment.call_args.kwargs + assert kwargs["name"] == "exp" + assert kwargs["data"] is ctx_data + assert kwargs["metadata"] == {"sha": "abc123"} + assert kwargs["_dataset_version"] == ctx_version + assert kwargs["task"] is _noop_task + + def test_call_overrides_win(self): + ctx = _make_ctx( + data=[{"input": "ctx"}], + dataset_version=datetime(2026, 1, 1), + ) + + override_data = [{"input": "override"}] + override_version = datetime(2026, 6, 6) + ctx.run_experiment( + name="exp", + task=_noop_task, + run_name="call-run", + data=override_data, + _dataset_version=override_version, + ) + + kwargs = ctx.client.run_experiment.call_args.kwargs + assert kwargs["name"] == "exp" + assert kwargs["run_name"] == "call-run" + assert kwargs["data"] is override_data + assert kwargs["_dataset_version"] == override_version + + +class TestRunnerContextMetadataMerge: + def test_user_keys_win_on_collision(self): + ctx = _make_ctx( + data=[{"input": "a"}], + metadata={"sha": "abc", "branch": "main"}, + ) + ctx.run_experiment( + name="exp", task=_noop_task, metadata={"sha": "def", "pr": "42"} + ) + assert ctx.client.run_experiment.call_args.kwargs["metadata"] == { + "sha": "def", + "branch": "main", + "pr": "42", + } + + def test_context_metadata_only(self): + ctx = _make_ctx(data=[{"input": "a"}], metadata={"sha": "abc"}) + ctx.run_experiment(name="exp", task=_noop_task) + assert ctx.client.run_experiment.call_args.kwargs["metadata"] == {"sha": "abc"} + + def test_call_metadata_only(self): + ctx = _make_ctx(data=[{"input": "a"}]) + ctx.run_experiment(name="exp", task=_noop_task, metadata={"pr": "1"}) + assert ctx.client.run_experiment.call_args.kwargs["metadata"] == {"pr": "1"} + + def test_both_none_stays_none(self): + ctx = _make_ctx(data=[{"input": "a"}]) + ctx.run_experiment(name="exp", task=_noop_task) + assert ctx.client.run_experiment.call_args.kwargs["metadata"] is None + + +class TestRunnerContextLocalItems: + def test_local_items_pass_through_as_context_default(self): + items = [{"input": "x", "expected_output": "y"}] + ctx = _make_ctx(data=items) + ctx.run_experiment(name="exp", task=_noop_task) + assert ctx.client.run_experiment.call_args.kwargs["data"] is items + + def test_local_items_pass_through_as_call_override(self): + ctx = _make_ctx() + items = [{"input": "x"}] + ctx.run_experiment(name="exp", task=_noop_task, data=items) + assert ctx.client.run_experiment.call_args.kwargs["data"] is items + + +class TestRunnerContextValidation: + def test_missing_data_raises(self): + ctx = _make_ctx() + with pytest.raises(ValueError, match="data"): + ctx.run_experiment(name="exp", task=_noop_task) + + +class TestRegressionError: + def test_is_exception(self): + result = MagicMock() + exc = RegressionError(result=result) + assert isinstance(exc, Exception) + assert exc.result is result + + def test_default_message(self): + exc = RegressionError(result=MagicMock()) + assert str(exc) == "Experiment regression detected" + assert exc.metric is None + assert exc.value is None + assert exc.threshold is None + + def test_structured_message(self): + exc = RegressionError( + result=MagicMock(), metric="avg_accuracy", value=0.78, threshold=0.9 + ) + assert exc.metric == "avg_accuracy" + assert exc.value == 0.78 + assert exc.threshold == 0.9 + assert "avg_accuracy" in str(exc) + assert "0.78" in str(exc) + assert "0.9" in str(exc) + + def test_free_form_message(self): + exc = RegressionError( + result=MagicMock(), + message="custom explanation", + ) + assert str(exc) == "custom explanation" + + def test_message_wins_over_structured(self): + exc = RegressionError( + result=MagicMock(), + metric="avg_accuracy", + value=0.5, + threshold=0.9, + message="custom explanation", + ) + assert str(exc) == "custom explanation" + assert exc.metric == "avg_accuracy" + assert exc.value == 0.5 + assert exc.threshold == 0.9 + + def test_partial_structured_falls_back_to_default(self): + """The structured overload requires ``metric`` and ``value`` together. + + If a caller bypasses the type checker and passes only one, we fall + back to the default message rather than rendering misleading + ``None`` placeholders in the PR comment. + """ + exc = RegressionError(result=MagicMock(), metric="avg_accuracy") # type: ignore[call-overload] + assert str(exc) == "Experiment regression detected" + + +class TestSignatureDriftGuard: + """Fails loudly if ``Langfuse.run_experiment`` grows a parameter that is + not threaded through ``RunnerContext.run_experiment``. + + ``data`` is the only genuinely relaxed parameter: it is required on the + client but optional on the RunnerContext so the action can inject it. + ``run_name`` and ``_dataset_version`` are already ``Optional`` on the + client and must match as-is. ``name`` is required on both — the action + supports a directory of experiments, so each script must name itself. + """ + + RELAXED_PARAMS = {"data"} + + # `CompositeEvaluatorFunction` is only imported under TYPE_CHECKING in + # ``langfuse.experiment`` to break the circular dependency with + # ``langfuse.batch_evaluation``, so its forward-ref must be resolved + # explicitly when inspecting annotations. + LOCALNS = {"CompositeEvaluatorFunction": CompositeEvaluatorFunction} + + def test_no_divergence(self): + client_param_names = self._param_names(Langfuse.run_experiment) + ctx_param_names = self._param_names(RunnerContext.run_experiment) + + assert client_param_names == ctx_param_names, ( + "RunnerContext.run_experiment params do not match " + "Langfuse.run_experiment. Missing: " + f"{client_param_names - ctx_param_names}. " + f"Extra: {ctx_param_names - client_param_names}." + ) + + client_hints = get_type_hints(Langfuse.run_experiment) + ctx_hints = get_type_hints( + RunnerContext.run_experiment, localns=self.LOCALNS + ) + + for name in client_param_names: + client_ann = client_hints.get(name, inspect.Parameter.empty) + ctx_ann = ctx_hints.get(name, inspect.Parameter.empty) + + if name in self.RELAXED_PARAMS: + # RunnerContext version must be Optional[]. + # Already-optional client annotations (``run_name``, + # ``_dataset_version``) just need to match as-is. + if self._is_optional(client_ann): + assert ctx_ann == client_ann, ( + f"param `{name}`: expected {client_ann}, got {ctx_ann}" + ) + else: + assert ctx_ann == typing.Optional[client_ann], ( + f"param `{name}`: expected Optional[{client_ann}], " + f"got {ctx_ann}" + ) + else: + assert ctx_ann == client_ann, ( + f"param `{name}`: annotation drift — " + f"client={client_ann}, context={ctx_ann}" + ) + + @staticmethod + def _param_names(func) -> set: + return { + name + for name in inspect.signature(func).parameters + if name != "self" + } + + @staticmethod + def _is_optional(annotation) -> bool: + origin = typing.get_origin(annotation) + args = typing.get_args(annotation) + return origin is typing.Union and type(None) in args diff --git a/tests/unit/test_initialization.py b/tests/unit/test_initialization.py new file mode 100644 index 000000000..6664d318f --- /dev/null +++ b/tests/unit/test_initialization.py @@ -0,0 +1,299 @@ +"""Test suite for Langfuse client initialization with LANGFUSE_HOST and LANGFUSE_BASE_URL. + +This test suite verifies that both LANGFUSE_HOST (deprecated) and LANGFUSE_BASE_URL +environment variables work correctly for initializing the Langfuse client. +""" + +import os + +import pytest + +from langfuse import Langfuse +from langfuse._client.resource_manager import LangfuseResourceManager + + +class TestClientInitialization: + """Tests for Langfuse client initialization with different URL configurations.""" + + @pytest.fixture(autouse=True) + def cleanup_env_vars(self): + """Fixture to clean up environment variables and singleton cache before and after each test.""" + # Store original values + original_values = { + "LANGFUSE_BASE_URL": os.environ.get("LANGFUSE_BASE_URL"), + "LANGFUSE_HOST": os.environ.get("LANGFUSE_HOST"), + "LANGFUSE_PUBLIC_KEY": os.environ.get("LANGFUSE_PUBLIC_KEY"), + "LANGFUSE_SECRET_KEY": os.environ.get("LANGFUSE_SECRET_KEY"), + } + + # Remove LANGFUSE_BASE_URL and LANGFUSE_HOST for the test + # but keep PUBLIC_KEY and SECRET_KEY if they exist + for key in ["LANGFUSE_BASE_URL", "LANGFUSE_HOST"]: + if key in os.environ: + del os.environ[key] + + yield + + # Clear the singleton cache to prevent test pollution + with LangfuseResourceManager._lock: + LangfuseResourceManager._instances.clear() + + # Restore original values - always remove any test values first + for key in [ + "LANGFUSE_BASE_URL", + "LANGFUSE_HOST", + "LANGFUSE_PUBLIC_KEY", + "LANGFUSE_SECRET_KEY", + ]: + if key in os.environ: + del os.environ[key] + + # Then restore original values + for key, value in original_values.items(): + if value is not None: + os.environ[key] = value + + def test_base_url_parameter_takes_precedence(self, cleanup_env_vars): + """Test that base_url parameter takes highest precedence.""" + os.environ["LANGFUSE_BASE_URL"] = "http://env-base-url.com" + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + client = Langfuse( + base_url="http://param-base-url.com", + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://param-base-url.com" + + def test_env_base_url_takes_precedence_over_host_param(self, cleanup_env_vars): + """Test that LANGFUSE_BASE_URL env var takes precedence over host parameter.""" + os.environ["LANGFUSE_BASE_URL"] = "http://env-base-url.com" + + client = Langfuse( + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://env-base-url.com" + + def test_host_parameter_fallback(self, cleanup_env_vars): + """Test that host parameter works as fallback when base_url is not set.""" + client = Langfuse( + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://param-host.com" + + def test_env_host_fallback(self, cleanup_env_vars): + """Test that LANGFUSE_HOST env var works as fallback.""" + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://env-host.com" + + def test_default_base_url(self, cleanup_env_vars): + """Test that default base_url is used when nothing is set.""" + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "https://cloud.langfuse.com" + + def test_base_url_env_var(self, cleanup_env_vars): + """Test that LANGFUSE_BASE_URL environment variable is used correctly.""" + os.environ["LANGFUSE_BASE_URL"] = "http://test-base-url.com" + + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://test-base-url.com" + + def test_host_env_var(self, cleanup_env_vars): + """Test that LANGFUSE_HOST environment variable is used correctly (deprecated).""" + os.environ["LANGFUSE_HOST"] = "http://test-host.com" + + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://test-host.com" + + def test_base_url_parameter(self, cleanup_env_vars): + """Test that base_url parameter is used correctly.""" + client = Langfuse( + base_url="http://param-base-url.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://param-base-url.com" + + def test_precedence_order_all_set(self, cleanup_env_vars): + """Test complete precedence order: base_url param > env > host param > env > default.""" + os.environ["LANGFUSE_BASE_URL"] = "http://env-base-url.com" + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + # Case 1: base_url parameter wins + client1 = Langfuse( + base_url="http://param-base-url.com", + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client1._base_url == "http://param-base-url.com" + + # Case 2: LANGFUSE_BASE_URL env var wins when base_url param not set + client2 = Langfuse( + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client2._base_url == "http://env-base-url.com" + + def test_precedence_without_base_url(self, cleanup_env_vars): + """Test precedence when base_url options are not set.""" + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + # Case 1: host parameter wins + client1 = Langfuse( + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client1._base_url == "http://param-host.com" + + # Case 2: LANGFUSE_HOST env var is used + client2 = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + assert client2._base_url == "http://env-host.com" + + def test_url_used_in_api_client(self, cleanup_env_vars): + """Test that the resolved base_url is correctly passed to API clients.""" + test_url = "http://test-unique-api.com" + # Use a unique public key to avoid singleton conflicts + client = Langfuse( + base_url=test_url, + public_key=f"test_pk_{test_url}", + secret_key="test_sk", + ) + + # Check that the API client has the correct base_url + assert client.api._client_wrapper._base_url == test_url + assert client.async_api._client_wrapper._base_url == test_url + + def test_url_used_in_trace_url_generation(self, cleanup_env_vars): + """Test that the resolved base_url is stored correctly for trace URL generation.""" + test_url = "http://test-trace-api.com" + # Use a unique public key to avoid singleton conflicts + client = Langfuse( + base_url=test_url, + public_key=f"test_pk_{test_url}", + secret_key="test_sk", + ) + + # Verify that the base_url is stored correctly and will be used for URL generation + # We can't test the full URL generation without making network calls to get project_id + # but we can verify the base_url is correctly set + assert client._base_url == test_url + + def test_both_base_url_and_host_params(self, cleanup_env_vars): + """Test that base_url parameter takes precedence over host parameter.""" + client = Langfuse( + base_url="http://base-url.com", + host="http://host.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://base-url.com" + + def test_both_env_vars_set(self, cleanup_env_vars): + """Test that LANGFUSE_BASE_URL takes precedence over LANGFUSE_HOST.""" + os.environ["LANGFUSE_BASE_URL"] = "http://base-url.com" + os.environ["LANGFUSE_HOST"] = "http://host.com" + + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://base-url.com" + + def test_localhost_urls(self, cleanup_env_vars): + """Test that localhost URLs work correctly.""" + # Test with base_url + client1 = Langfuse( + base_url="http://localhost:3000", + public_key="test_pk", + secret_key="test_sk", + ) + assert client1._base_url == "http://localhost:3000" + + # Test with host (deprecated) + client2 = Langfuse( + host="http://localhost:3000", + public_key="test_pk", + secret_key="test_sk", + ) + assert client2._base_url == "http://localhost:3000" + + # Test with env var + os.environ["LANGFUSE_BASE_URL"] = "http://localhost:3000" + client3 = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + assert client3._base_url == "http://localhost:3000" + + def test_trailing_slash_handling(self, cleanup_env_vars): + """Test that URLs with trailing slashes are handled correctly.""" + # URLs with trailing slashes should work + client1 = Langfuse( + base_url="http://test.com/", + public_key="test_pk", + secret_key="test_sk", + ) + # The SDK should accept the URL as-is (API client will handle normalization) + assert client1._base_url == "http://test.com/" + + def test_urls_with_paths(self, cleanup_env_vars): + """Test that URLs with paths work correctly.""" + client = Langfuse( + base_url="http://test.com/api/v1", + public_key="test_pk", + secret_key="test_sk", + ) + assert client._base_url == "http://test.com/api/v1" + + def test_https_and_http_urls(self, cleanup_env_vars): + """Test that both HTTPS and HTTP URLs work.""" + # HTTPS + client1 = Langfuse( + base_url="https://secure.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client1._base_url == "https://secure.com" + + # HTTP + client2 = Langfuse( + base_url="http://insecure.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client2._base_url == "http://insecure.com" diff --git a/tests/test_json.py b/tests/unit/test_json.py similarity index 90% rename from tests/test_json.py rename to tests/unit/test_json.py index bf0e38c65..1f7ef6ece 100644 --- a/tests/test_json.py +++ b/tests/unit/test_json.py @@ -7,13 +7,12 @@ from unittest.mock import patch import pytest -from bson import ObjectId -from langchain.schema.messages import HumanMessage +from langchain.messages import HumanMessage from pydantic import BaseModel import langfuse from langfuse._utils.serializer import EventSerializer -from langfuse.api.resources.commons.types.observation_level import ObservationLevel +from langfuse.api import ObservationLevel class TestModel(BaseModel): @@ -32,8 +31,9 @@ def test_json_encoder(): } result = json.dumps(obj, cls=EventSerializer) + print(result) assert ( - '{"foo": "bar", "bar": "2021-01-01T00:00:00Z", "date": "2024-01-01", "messages": [{"content": "I love programming!", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}]}' + '{"foo": "bar", "bar": "2021-01-01T00:00:00Z", "date": "2024-01-01", "messages": [{"content": "I love programming!", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null}]}' in result ) @@ -129,11 +129,3 @@ def test_observation_level(): result = json.dumps(ObservationLevel.ERROR, cls=EventSerializer) assert result == '"ERROR"' - - -def test_mongo_cursor(): - test_id = ObjectId("5f3e3e3e3e3e3e3e3e3e3e3e") - - result = json.dumps(test_id, cls=EventSerializer) - - assert isinstance(result, str) diff --git a/tests/unit/test_langchain.py b/tests/unit/test_langchain.py new file mode 100644 index 000000000..27298342c --- /dev/null +++ b/tests/unit/test_langchain.py @@ -0,0 +1,850 @@ +import importlib +from contextvars import copy_context +from unittest.mock import patch +from uuid import uuid4 + +import pytest +from langchain.messages import HumanMessage +from langchain_core.messages import AIMessage +from langchain_core.output_parsers import StrOutputParser +from langchain_core.outputs import ChatGeneration, ChatResult, Generation, LLMResult +from langchain_core.prompts import ChatPromptTemplate +from langchain_openai import ChatOpenAI, OpenAI +from opentelemetry import context as otel_context + +from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse.langchain import CallbackHandler + +callback_handler_module = importlib.import_module("langfuse.langchain.CallbackHandler") + + +def _assert_parent_child(parent_span, child_span) -> None: + assert child_span.parent is not None + assert child_span.parent.span_id == parent_span.context.span_id + + +def _has_pending_resume_context(handler, resume_key: str) -> bool: + return resume_key in handler._pending_resume_trace_contexts + + +def _pending_resume_context_keys(handler) -> list[str]: + return handler._pending_resume_trace_contexts.keys() + + +def _get_root_resume_key(handler, root_run_id): + root_run_state = handler._root_run_states.get(root_run_id) + return None if root_run_state is None else root_run_state.resume_key + + +def _has_run_state(handler, run_id) -> bool: + return run_id in handler._run_states + + +def test_chat_model_callback_exports_generation_span( + langfuse_memory_client, get_span, json_attr +): + response = ChatResult( + generations=[ + ChatGeneration(message=AIMessage(content="bonjour"), text="bonjour") + ], + llm_output={ + "token_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + }, + "model_name": "gpt-4o-mini", + }, + ) + + with patch.object(ChatOpenAI, "_generate", return_value=response): + handler = CallbackHandler() + + with langfuse_memory_client.start_as_current_observation(name="parent"): + ChatOpenAI(api_key="test", temperature=0).invoke( + [HumanMessage(content="hello")], + config={"callbacks": [handler]}, + ) + + langfuse_memory_client.flush() + parent_span = get_span("parent") + generation_span = get_span("ChatOpenAI") + + _assert_parent_child(parent_span, generation_span) + assert ( + generation_span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] + == "generation" + ) + assert json_attr(generation_span, LangfuseOtelSpanAttributes.OBSERVATION_INPUT) == [ + {"role": "user", "content": "hello"} + ] + assert json_attr( + generation_span, LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT + ) == { + "role": "assistant", + "content": "bonjour", + } + assert ( + generation_span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] + == "gpt-4o-mini" + ) + assert json_attr( + generation_span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS + ) == { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + } + + +def test_llm_callback_exports_generation_span(langfuse_memory_client, get_span): + response = LLMResult( + generations=[[Generation(text="sockzilla")]], + llm_output={ + "token_usage": { + "prompt_tokens": 7, + "completion_tokens": 3, + "total_tokens": 10, + }, + "model_name": "gpt-4o-mini-instruct", + }, + ) + + with patch.object(OpenAI, "_generate", return_value=response): + handler = CallbackHandler() + + with langfuse_memory_client.start_as_current_observation(name="parent"): + OpenAI(api_key="test", temperature=0).invoke( + "name a sock company", + config={"callbacks": [handler], "run_name": "sock-name"}, + ) + + langfuse_memory_client.flush() + span = get_span("sock-name") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "sockzilla" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] + == "gpt-4o-mini-instruct" + ) + + +def test_lcel_chain_exports_intermediate_chain_spans( + langfuse_memory_client, get_span, find_spans +): + response = ChatResult( + generations=[ + ChatGeneration( + message=AIMessage(content="knock knock"), + text="knock knock", + ) + ], + llm_output={ + "token_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + }, + "model_name": "gpt-4o-mini", + }, + ) + + with patch.object(ChatOpenAI, "_generate", return_value=response): + handler = CallbackHandler() + prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}") + chain = prompt | ChatOpenAI(api_key="test", temperature=0) | StrOutputParser() + + with langfuse_memory_client.start_as_current_observation(name="parent"): + result = chain.invoke({"topic": "otters"}, config={"callbacks": [handler]}) + + assert result == "knock knock" + + langfuse_memory_client.flush() + sequence_span = get_span("RunnableSequence") + prompt_span = get_span("ChatPromptTemplate") + generation_span = get_span("ChatOpenAI") + parser_span = get_span("StrOutputParser") + + _assert_parent_child(sequence_span, prompt_span) + _assert_parent_child(sequence_span, generation_span) + _assert_parent_child(sequence_span, parser_span) + assert len(find_spans("ChatOpenAI")) == 1 + + +def test_chat_model_error_marks_generation_error(langfuse_memory_client, get_span): + with patch.object(ChatOpenAI, "_generate", side_effect=RuntimeError("boom")): + handler = CallbackHandler() + + with langfuse_memory_client.start_as_current_observation(name="parent"): + with pytest.raises(RuntimeError, match="boom"): + ChatOpenAI(api_key="test", temperature=0).invoke( + [HumanMessage(content="hello")], + config={"callbacks": [handler]}, + ) + + langfuse_memory_client.flush() + span = get_span("ChatOpenAI") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" + assert ( + "boom" in span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + ) + + +def test_root_chain_metadata_propagates_trace_name( + langfuse_memory_client, get_span, find_spans +): + response = ChatResult( + generations=[ + ChatGeneration( + message=AIMessage(content="knock knock"), + text="knock knock", + ) + ], + llm_output={ + "token_usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + }, + "model_name": "gpt-4o-mini", + }, + ) + + with patch.object(ChatOpenAI, "_generate", return_value=response): + handler = CallbackHandler() + prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}") + chain = prompt | ChatOpenAI(api_key="test", temperature=0) | StrOutputParser() + + result = chain.invoke( + {"topic": "otters"}, + config={ + "callbacks": [handler], + "metadata": {"langfuse_trace_name": "langchain-trace-name"}, + }, + ) + + assert result == "knock knock" + + langfuse_memory_client.flush() + root_span = get_span("RunnableSequence") + generation_span = get_span("ChatOpenAI") + + assert ( + root_span.attributes[LangfuseOtelSpanAttributes.TRACE_NAME] + == "langchain-trace-name" + ) + assert ( + generation_span.attributes[LangfuseOtelSpanAttributes.TRACE_NAME] + == "langchain-trace-name" + ) + assert ( + f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langfuse_trace_name" + not in root_span.attributes + ) + assert len(find_spans("ChatOpenAI")) == 1 + + +def test_root_chain_exports_when_end_runs_in_copied_context( + langfuse_memory_client, get_span +): + handler = CallbackHandler() + run_id = uuid4() + + handler.on_chain_start( + {"id": ["RunnableSequence"]}, + {"topic": "otters"}, + run_id=run_id, + metadata={"langfuse_trace_name": "async-root-trace"}, + ) + + copy_context().run( + handler.on_chain_end, + {"output": "knock knock"}, + run_id=run_id, + ) + + langfuse_memory_client.flush() + root_span = get_span("RunnableSequence") + + assert root_span.attributes[LangfuseOtelSpanAttributes.TRACE_NAME] == ( + "async-root-trace" + ) + + +def test_control_flow_errors_use_default_level_and_keep_status_message( + langfuse_memory_client, get_span, monkeypatch +): + class DummyControlFlowError(RuntimeError): + pass + + monkeypatch.setattr( + callback_handler_module, + "CONTROL_FLOW_EXCEPTION_TYPES", + {DummyControlFlowError}, + ) + + handler = CallbackHandler() + + tool_run_id = uuid4() + retriever_run_id = uuid4() + llm_run_id = uuid4() + chain_run_id = uuid4() + + handler.on_tool_start( + {"name": "human_approval"}, + "{}", + run_id=tool_run_id, + ) + handler.on_tool_error( + DummyControlFlowError("tool interrupt"), + run_id=tool_run_id, + ) + + handler.on_retriever_start( + {"name": "knowledge_base"}, + "approval policy", + run_id=retriever_run_id, + ) + handler.on_retriever_error( + DummyControlFlowError("retriever bubble-up"), + run_id=retriever_run_id, + ) + + handler.on_llm_start( + {"name": "TestLLM"}, + ["need approval"], + run_id=llm_run_id, + invocation_params={}, + ) + handler.on_llm_error( + DummyControlFlowError("llm bubble-up"), + run_id=llm_run_id, + ) + + handler.on_chain_start( + {"name": "LangGraph"}, + {"messages": ["need approval"]}, + run_id=chain_run_id, + ) + handler.on_chain_error( + DummyControlFlowError("graph interrupt"), + run_id=chain_run_id, + ) + + handler._langfuse_client.flush() + + for span_name, message in [ + ("human_approval", "tool interrupt"), + ("knowledge_base", "retriever bubble-up"), + ("TestLLM", "llm bubble-up"), + ("LangGraph", "graph interrupt"), + ]: + span = get_span(span_name) + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "DEFAULT" + ) + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == message + ) + + +def test_control_flow_resume_uses_thread_keyed_explicit_resume_context( + memory_exporter, langfuse_memory_client, monkeypatch +): + class DummyControlFlowError(RuntimeError): + pass + + Command = pytest.importorskip("langgraph.types").Command + + context_token = otel_context.attach(otel_context.Context()) + monkeypatch.setattr( + callback_handler_module, + "CONTROL_FLOW_EXCEPTION_TYPES", + {DummyControlFlowError}, + ) + + try: + handler = CallbackHandler() + + thread_one_interrupt_run_id = uuid4() + thread_two_interrupt_run_id = uuid4() + thread_one_fresh_run_id = uuid4() + thread_two_resume_run_id = uuid4() + thread_one_resume_run_id = uuid4() + + handler.on_chain_start( + {"name": "LangGraph"}, + {"messages": ["need approval"]}, + run_id=thread_one_interrupt_run_id, + metadata={"thread_id": "thread-1"}, + ) + handler.on_chain_error( + DummyControlFlowError("graph interrupt 1"), + run_id=thread_one_interrupt_run_id, + ) + + handler.on_chain_start( + {"name": "LangGraph"}, + {"messages": ["need approval"]}, + run_id=thread_two_interrupt_run_id, + metadata={"thread_id": "thread-2"}, + ) + handler.on_chain_error( + DummyControlFlowError("graph interrupt 2"), + run_id=thread_two_interrupt_run_id, + ) + + handler.on_chain_start( + {"name": "LangGraph"}, + {"messages": ["fresh invocation"]}, + run_id=thread_one_fresh_run_id, + metadata={"thread_id": "thread-1"}, + ) + handler.on_chain_end( + {"messages": ["completed"]}, + run_id=thread_one_fresh_run_id, + ) + + handler.on_chain_start( + {"name": "LangGraph"}, + Command(resume={"approved": True}), + run_id=thread_two_resume_run_id, + metadata={"thread_id": "thread-2"}, + ) + handler.on_chain_end( + {"messages": ["approved"]}, + run_id=thread_two_resume_run_id, + ) + + handler.on_chain_start( + {"name": "LangGraph"}, + Command(resume={"approved": True}), + run_id=thread_one_resume_run_id, + metadata={"thread_id": "thread-1"}, + ) + handler.on_chain_end( + {"messages": ["approved"]}, + run_id=thread_one_resume_run_id, + ) + + handler._langfuse_client.flush() + + root_spans = [ + span + for span in memory_exporter.get_finished_spans() + if span.name == "LangGraph" + ] + + assert len(root_spans) == 5 + spans_by_trace_id = {} + for span in root_spans: + spans_by_trace_id.setdefault(span.context.trace_id, []).append(span) + + assert sorted(len(spans) for spans in spans_by_trace_id.values()) == [1, 2, 2] + + resumed_trace_spans = [ + spans for spans in spans_by_trace_id.values() if len(spans) == 2 + ] + assert len(resumed_trace_spans) == 2 + + for spans in resumed_trace_spans: + initial_span = next(span for span in spans if span.parent is None) + resumed_span = next(span for span in spans if span.parent is not None) + assert resumed_span.parent.span_id == initial_span.context.span_id + + fresh_trace_spans = next( + spans for spans in spans_by_trace_id.values() if len(spans) == 1 + ) + assert fresh_trace_spans[0].parent is None + finally: + otel_context.detach(context_token) + + +def test_control_flow_resume_restores_context_after_failed_root_start( + memory_exporter, langfuse_memory_client, monkeypatch +): + class DummyControlFlowError(RuntimeError): + pass + + Command = pytest.importorskip("langgraph.types").Command + + context_token = otel_context.attach(otel_context.Context()) + monkeypatch.setattr( + callback_handler_module, + "CONTROL_FLOW_EXCEPTION_TYPES", + {DummyControlFlowError}, + ) + + try: + handler = CallbackHandler() + + interrupt_run_id = uuid4() + failed_resume_run_id = uuid4() + successful_resume_run_id = uuid4() + + handler.on_chain_start( + {"name": "LangGraph"}, + {"messages": ["need approval"]}, + run_id=interrupt_run_id, + metadata={"thread_id": "thread-1"}, + ) + handler.on_chain_error( + DummyControlFlowError("graph interrupt"), + run_id=interrupt_run_id, + ) + + assert _has_pending_resume_context(handler, "thread-1") + + with patch.object( + handler._langfuse_client, + "start_observation", + side_effect=RuntimeError("trace create failed"), + ): + handler.on_chain_start( + {"name": "LangGraph"}, + Command(resume={"approved": True}), + run_id=failed_resume_run_id, + metadata={"thread_id": "thread-1"}, + ) + + assert _has_pending_resume_context(handler, "thread-1") + assert _get_root_resume_key(handler, failed_resume_run_id) is None + assert not _has_run_state(handler, failed_resume_run_id) + assert failed_resume_run_id not in handler._root_run_states + + handler.on_chain_start( + {"name": "LangGraph"}, + Command(resume={"approved": True}), + run_id=successful_resume_run_id, + metadata={"thread_id": "thread-1"}, + ) + handler.on_chain_end( + {"messages": ["approved"]}, + run_id=successful_resume_run_id, + ) + + handler._langfuse_client.flush() + + root_spans = [ + span + for span in memory_exporter.get_finished_spans() + if span.name == "LangGraph" + ] + + assert len(root_spans) == 2 + + initial_span = next(span for span in root_spans if span.parent is None) + resumed_span = next(span for span in root_spans if span.parent is not None) + + assert resumed_span.parent.span_id == initial_span.context.span_id + finally: + otel_context.detach(context_token) + + +def test_control_flow_resume_ignores_non_resume_commands( + memory_exporter, langfuse_memory_client, monkeypatch +): + class DummyControlFlowError(RuntimeError): + pass + + Command = pytest.importorskip("langgraph.types").Command + + context_token = otel_context.attach(otel_context.Context()) + monkeypatch.setattr( + callback_handler_module, + "CONTROL_FLOW_EXCEPTION_TYPES", + {DummyControlFlowError}, + ) + + try: + handler = CallbackHandler() + + interrupt_run_id = uuid4() + goto_run_id = uuid4() + resume_run_id = uuid4() + + handler.on_chain_start( + {"name": "LangGraph"}, + {"messages": ["need approval"]}, + run_id=interrupt_run_id, + metadata={"thread_id": "thread-1"}, + ) + handler.on_chain_error( + DummyControlFlowError("graph interrupt"), + run_id=interrupt_run_id, + ) + + handler.on_chain_start( + {"name": "LangGraph"}, + Command(goto="approval_node"), + run_id=goto_run_id, + metadata={"thread_id": "thread-1"}, + ) + handler.on_chain_end( + {"messages": ["routed"]}, + run_id=goto_run_id, + ) + + assert _has_pending_resume_context(handler, "thread-1") + + handler.on_chain_start( + {"name": "LangGraph"}, + Command(resume={"approved": True}), + run_id=resume_run_id, + metadata={"thread_id": "thread-1"}, + ) + handler.on_chain_end( + {"messages": ["approved"]}, + run_id=resume_run_id, + ) + + handler._langfuse_client.flush() + + root_spans = [ + span + for span in memory_exporter.get_finished_spans() + if span.name == "LangGraph" + ] + + assert len(root_spans) == 3 + + spans_by_trace_id = {} + for span in root_spans: + spans_by_trace_id.setdefault(span.context.trace_id, []).append(span) + + assert sorted(len(spans) for spans in spans_by_trace_id.values()) == [1, 2] + + resumed_trace_spans = next( + spans for spans in spans_by_trace_id.values() if len(spans) == 2 + ) + initial_span = next(span for span in resumed_trace_spans if span.parent is None) + resumed_span = next( + span for span in resumed_trace_spans if span.parent is not None + ) + + assert resumed_span.parent.span_id == initial_span.context.span_id + finally: + otel_context.detach(context_token) + + +def test_root_reset_preserves_other_inflight_resume_keys( + memory_exporter, langfuse_memory_client, monkeypatch +): + class DummyControlFlowError(RuntimeError): + pass + + Command = pytest.importorskip("langgraph.types").Command + + context_token = otel_context.attach(otel_context.Context()) + monkeypatch.setattr( + callback_handler_module, + "CONTROL_FLOW_EXCEPTION_TYPES", + {DummyControlFlowError}, + ) + + try: + handler = CallbackHandler() + root_one_context = copy_context() + root_two_context = copy_context() + + root_one_run_id = uuid4() + root_two_run_id = uuid4() + root_two_resume_run_id = uuid4() + + root_one_context.run( + handler.on_chain_start, + {"name": "LangGraph"}, + {"messages": ["completed"]}, + run_id=root_one_run_id, + metadata={"thread_id": "thread-1"}, + ) + root_two_context.run( + handler.on_chain_start, + {"name": "LangGraph"}, + {"messages": ["need approval"]}, + run_id=root_two_run_id, + metadata={"thread_id": "thread-2"}, + ) + + assert _get_root_resume_key(handler, root_two_run_id) == "thread-2" + + root_one_context.run( + handler.on_chain_end, + {"messages": ["completed"]}, + run_id=root_one_run_id, + ) + + assert _get_root_resume_key(handler, root_two_run_id) == "thread-2" + + root_two_context.run( + handler.on_chain_error, + DummyControlFlowError("graph interrupt"), + run_id=root_two_run_id, + ) + + assert _has_pending_resume_context(handler, "thread-2") + + root_two_context.run( + handler.on_chain_start, + {"name": "LangGraph"}, + Command(resume={"approved": True}), + run_id=root_two_resume_run_id, + metadata={"thread_id": "thread-2"}, + ) + root_two_context.run( + handler.on_chain_end, + {"messages": ["approved"]}, + run_id=root_two_resume_run_id, + ) + + handler._langfuse_client.flush() + + root_spans = [ + span + for span in memory_exporter.get_finished_spans() + if span.name == "LangGraph" + ] + + assert len(root_spans) == 3 + + spans_by_trace_id = {} + for span in root_spans: + spans_by_trace_id.setdefault(span.context.trace_id, []).append(span) + + assert sorted(len(spans) for spans in spans_by_trace_id.values()) == [1, 2] + finally: + otel_context.detach(context_token) + + +def test_root_tool_and_retriever_runs_seed_resume_keys_and_cleanup( + langfuse_memory_client, monkeypatch +): + class DummyControlFlowError(RuntimeError): + pass + + monkeypatch.setattr( + callback_handler_module, + "CONTROL_FLOW_EXCEPTION_TYPES", + {DummyControlFlowError}, + ) + + handler = CallbackHandler() + + tool_error_run_id = uuid4() + tool_end_run_id = uuid4() + retriever_run_id = uuid4() + + handler.on_tool_start( + {"name": "human_approval"}, + "{}", + run_id=tool_error_run_id, + metadata={"thread_id": "tool-error-thread"}, + ) + assert _get_root_resume_key(handler, tool_error_run_id) == "tool-error-thread" + + handler.on_tool_error( + DummyControlFlowError("tool interrupt"), + run_id=tool_error_run_id, + ) + + assert _has_pending_resume_context(handler, "tool-error-thread") + assert _get_root_resume_key(handler, tool_error_run_id) is None + assert not _has_run_state(handler, tool_error_run_id) + + handler.on_tool_start( + {"name": "human_approval"}, + "{}", + run_id=tool_end_run_id, + metadata={"thread_id": "tool-end-thread"}, + ) + assert _get_root_resume_key(handler, tool_end_run_id) == "tool-end-thread" + + handler.on_tool_end( + '{"approved": true}', + run_id=tool_end_run_id, + ) + + assert _get_root_resume_key(handler, tool_end_run_id) is None + assert not _has_run_state(handler, tool_end_run_id) + + handler.on_retriever_start( + {"name": "knowledge_base"}, + "approval policy", + run_id=retriever_run_id, + metadata={"thread_id": "retriever-thread"}, + ) + assert _get_root_resume_key(handler, retriever_run_id) == "retriever-thread" + + handler.on_retriever_error( + DummyControlFlowError("retriever interrupt"), + run_id=retriever_run_id, + ) + + assert _has_pending_resume_context(handler, "retriever-thread") + assert _get_root_resume_key(handler, retriever_run_id) is None + assert not _has_run_state(handler, retriever_run_id) + + +def test_pending_resume_contexts_are_capped(langfuse_memory_client, monkeypatch): + class DummyControlFlowError(RuntimeError): + pass + + monkeypatch.setattr( + callback_handler_module, + "CONTROL_FLOW_EXCEPTION_TYPES", + {DummyControlFlowError}, + ) + monkeypatch.setattr( + callback_handler_module, + "MAX_PENDING_RESUME_TRACE_CONTEXTS", + 4, + ) + + handler = CallbackHandler() + + for index in range(5): + run_id = uuid4() + thread_id = f"thread-{index}" + + handler.on_chain_start( + {"name": "LangGraph"}, + {"messages": ["need approval"]}, + run_id=run_id, + metadata={"thread_id": thread_id}, + ) + handler.on_chain_error( + DummyControlFlowError(f"graph interrupt {index}"), + run_id=run_id, + ) + + assert len(handler._pending_resume_trace_contexts) == 4 + assert _pending_resume_context_keys(handler) == [ + "thread-1", + "thread-2", + "thread-3", + "thread-4", + ] + + +def test_graphbubbleup_import_is_independent_from_command_import(): + real_import = __import__ + + def import_without_langgraph_command( + name, globals=None, locals=None, fromlist=(), level=0 + ): + if name == "langgraph.types": + raise ImportError("Command unavailable") + + return real_import(name, globals, locals, fromlist, level) + + with patch("builtins.__import__", side_effect=import_without_langgraph_command): + reloaded_module = importlib.reload(callback_handler_module) + assert reloaded_module.LANGGRAPH_COMMAND_TYPE is None + assert any( + exception_type.__name__ == "GraphBubbleUp" + for exception_type in reloaded_module.CONTROL_FLOW_EXCEPTION_TYPES + ) + + importlib.reload(callback_handler_module) diff --git a/tests/test_logger.py b/tests/unit/test_logger.py similarity index 100% rename from tests/test_logger.py rename to tests/unit/test_logger.py diff --git a/tests/test_media.py b/tests/unit/test_media.py similarity index 63% rename from tests/test_media.py rename to tests/unit/test_media.py index 088e88334..63df03920 100644 --- a/tests/test_media.py +++ b/tests/unit/test_media.py @@ -1,12 +1,10 @@ import base64 -import re -from uuid import uuid4 +from types import SimpleNamespace +from unittest.mock import Mock import pytest -from langfuse._client.client import Langfuse from langfuse.media import LangfuseMedia -from tests.utils import get_api # Test data SAMPLE_JPEG_BYTES = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00" @@ -109,59 +107,35 @@ def test_nonexistent_file(): assert media._content_type is None -def test_replace_media_reference_string_in_object(): - # Create test audio file - audio_file = "static/joke_prompt.wav" - with open(audio_file, "rb") as f: - mock_audio_bytes = f.read() - - # Create Langfuse client and trace with media - langfuse = Langfuse() - - mock_trace_name = f"test-trace-with-audio-{uuid4()}" - base64_audio = base64.b64encode(mock_audio_bytes).decode() - - span = langfuse.start_span( - name=mock_trace_name, - metadata={ - "context": { - "nested": LangfuseMedia( - base64_data_uri=f"data:audio/wav;base64,{base64_audio}" - ) - } - }, - ).end() - - langfuse.flush() - - # Verify media reference string format - fetched_trace = get_api().trace.get(span.trace_id) - media_ref = fetched_trace.observations[0].metadata["context"]["nested"] - assert re.match( - r"^@@@langfuseMedia:type=audio/wav\|id=.+\|source=base64_data_uri@@@$", - media_ref, - ) +def test_resolve_media_references_uses_configured_httpx_client(): + reference_string = "@@@langfuseMedia:type=image/jpeg|id=test-id|source=bytes@@@" + fetch_timeout_seconds = 7 - # Resolve media references back to base64 - resolved_obs = langfuse.resolve_media_references( - obj=fetched_trace.observations[0], resolve_with="base64_data_uri" + media_api = Mock() + media_api.get.return_value = SimpleNamespace( + url="https://example.com/test.jpg", content_type="image/jpeg" ) - # Verify resolved base64 matches original - expected_base64 = f"data:audio/wav;base64,{base64_audio}" - assert resolved_obs["metadata"]["context"]["nested"] == expected_base64 + response = Mock() + response.content = b"test-bytes" + response.raise_for_status.return_value = None - # Create second trace reusing the media reference - span2 = langfuse.start_span( - name=f"2-{mock_trace_name}", - metadata={"context": {"nested": resolved_obs["metadata"]["context"]["nested"]}}, - ).end() + httpx_client = Mock() + httpx_client.get.return_value = response - langfuse.flush() + mock_langfuse_client = SimpleNamespace( + api=SimpleNamespace(media=media_api), + _resources=SimpleNamespace(httpx_client=httpx_client), + ) - # Verify second trace has same media reference - fetched_trace2 = get_api().trace.get(span2.trace_id) - assert ( - fetched_trace2.observations[0].metadata["context"]["nested"] - == fetched_trace.observations[0].metadata["context"]["nested"] + resolved = LangfuseMedia.resolve_media_references( + obj={"image": reference_string}, + langfuse_client=mock_langfuse_client, + resolve_with="base64_data_uri", + content_fetch_timeout_seconds=fetch_timeout_seconds, + ) + + assert resolved["image"] == "data:image/jpeg;base64,dGVzdC1ieXRlcw==" + httpx_client.get.assert_called_once_with( + "https://example.com/test.jpg", timeout=fetch_timeout_seconds ) diff --git a/tests/unit/test_media_manager.py b/tests/unit/test_media_manager.py new file mode 100644 index 000000000..68684fac4 --- /dev/null +++ b/tests/unit/test_media_manager.py @@ -0,0 +1,196 @@ +from queue import Queue +from types import SimpleNamespace +from unittest.mock import Mock + +import httpx +import pytest + +from langfuse._task_manager.media_manager import MediaManager +from langfuse.media import LangfuseMedia + + +def _upload_response(status_code: int, text: str = "") -> httpx.Response: + request = httpx.Request("PUT", "https://example.com/upload") + return httpx.Response(status_code=status_code, request=request, text=text) + + +def _upload_job() -> dict: + return { + "media_id": "media-id", + "content_bytes": b"payload", + "content_type": "image/jpeg", + "content_length": 7, + "content_sha256_hash": "sha256hash", + "trace_id": "trace-id", + "observation_id": None, + "field": "input", + } + + +def test_media_upload_retries_on_retryable_http_status(): + media_api = Mock() + media_api.get_upload_url.return_value = SimpleNamespace( + upload_url="https://example.com/upload", + media_id="media-id", + ) + media_api.patch.return_value = None + + httpx_client = Mock() + httpx_client.put.side_effect = [ + _upload_response(503, "temporary failure"), + _upload_response(200, "ok"), + ] + + manager = MediaManager( + api_client=SimpleNamespace(media=media_api), + httpx_client=httpx_client, + media_upload_queue=Queue(), + max_retries=3, + ) + + manager._process_upload_media_job(data=_upload_job()) + + assert httpx_client.put.call_count == 2 + media_api.patch.assert_called_once() + assert media_api.patch.call_args.kwargs["upload_http_status"] == 200 + + +def test_media_upload_gives_up_on_non_retryable_http_status(): + media_api = Mock() + media_api.get_upload_url.return_value = SimpleNamespace( + upload_url="https://example.com/upload", + media_id="media-id", + ) + media_api.patch.return_value = None + + httpx_client = Mock() + httpx_client.put.return_value = _upload_response(403, "forbidden") + + manager = MediaManager( + api_client=SimpleNamespace(media=media_api), + httpx_client=httpx_client, + media_upload_queue=Queue(), + max_retries=3, + ) + + with pytest.raises(httpx.HTTPStatusError): + manager._process_upload_media_job(data=_upload_job()) + + assert httpx_client.put.call_count == 1 + media_api.patch.assert_called_once() + assert media_api.patch.call_args.kwargs["upload_http_status"] == 403 + + +def test_find_and_process_media_sse_done_passes_through(): + queue = Queue() + manager = MediaManager( + api_client=SimpleNamespace(media=Mock()), + httpx_client=Mock(), + media_upload_queue=queue, + ) + + data = "data: [DONE]" + result = manager._find_and_process_media( + data=data, trace_id="trace-id", observation_id=None, field="output" + ) + + assert result == data + assert queue.empty() + + +def test_find_and_process_media_sse_json_payload_passes_through(): + queue = Queue() + manager = MediaManager( + api_client=SimpleNamespace(media=Mock()), + httpx_client=Mock(), + media_upload_queue=queue, + ) + + plain_sse = 'data: {"choices": [{"delta": {"content": "hello"}}]}' + result = manager._find_and_process_media( + data=plain_sse, trace_id="trace-id", observation_id=None, field="output" + ) + assert result == plain_sse + + tricky_sse = 'data: {"text": "what is ;base64 encoding?", "count": 1}' + result = manager._find_and_process_media( + data=tricky_sse, trace_id="trace-id", observation_id=None, field="output" + ) + assert result == tricky_sse + assert queue.empty() + + +def test_find_and_process_media_valid_base64_uri_is_processed(): + queue = Queue() + manager = MediaManager( + api_client=SimpleNamespace(media=Mock()), + httpx_client=Mock(), + media_upload_queue=queue, + ) + + valid_base64_data_uri = ( + "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QBARXhpZgAA" + ) + result = manager._find_and_process_media( + data=valid_base64_data_uri, + trace_id="trace-id", + observation_id=None, + field="input", + ) + + assert isinstance(result, LangfuseMedia) + assert not queue.empty() + + +def test_find_and_process_media_data_uri_without_comma_passes_through(): + queue = Queue() + manager = MediaManager( + api_client=SimpleNamespace(media=Mock()), + httpx_client=Mock(), + media_upload_queue=queue, + ) + + data = "data:image/png;base64" + result = manager._find_and_process_media( + data=data, trace_id="trace-id", observation_id=None, field="input" + ) + + assert result == data + assert queue.empty() + + +def test_find_and_process_media_non_base64_data_uri_passes_through(): + queue = Queue() + manager = MediaManager( + api_client=SimpleNamespace(media=Mock()), + httpx_client=Mock(), + media_upload_queue=queue, + ) + + data = "data:text/plain,hello world" + result = manager._find_and_process_media( + data=data, trace_id="trace-id", observation_id=None, field="input" + ) + + assert result == data + assert queue.empty() + + +def test_find_and_process_media_sse_in_nested_structure_passes_through(): + queue = Queue() + manager = MediaManager( + api_client=SimpleNamespace(media=Mock()), + httpx_client=Mock(), + media_upload_queue=queue, + ) + + data = [ + {"role": "assistant", "content": "data: [DONE]"}, + {"role": "user", "content": "data: hello"}, + ] + result = manager._find_and_process_media( + data=data, trace_id="trace-id", observation_id=None, field="output" + ) + + assert result == data + assert queue.empty() diff --git a/tests/unit/test_observe.py b/tests/unit/test_observe.py new file mode 100644 index 000000000..24f79c3fc --- /dev/null +++ b/tests/unit/test_observe.py @@ -0,0 +1,315 @@ +import asyncio +import contextvars +import gc +import sys +from typing import Any, AsyncGenerator, Generator, cast + +import pytest + +from langfuse import observe +from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse._client.observe import ( + _ContextPreservedAsyncGeneratorWrapper, + _ContextPreservedSyncGeneratorWrapper, +) + + +class SpanRecorder: + def __init__(self) -> None: + self.ended = 0 + self.updates: list[dict[str, Any]] = [] + + def update(self, **kwargs: Any) -> "SpanRecorder": + self.updates.append(kwargs) + return self + + def end(self) -> "SpanRecorder": + self.ended += 1 + return self + + +def _finished_spans_by_name(memory_exporter: Any, name: str) -> list[Any]: + return [span for span in memory_exporter.get_finished_spans() if span.name == name] + + +def test_sync_generator_preserves_context_without_output_capture( + langfuse_memory_client: Any, memory_exporter: Any +) -> None: + @observe(name="child_step") + def child_step(index: int) -> str: + return f"item_{index}" + + @observe(name="root", capture_output=False) + def root() -> Generator[str, None, None]: + def body() -> Generator[str, None, None]: + for index in range(2): + yield child_step(index) + + return body() + + generator = root() + + assert memory_exporter.get_finished_spans() == [] + + assert list(generator) == ["item_0", "item_1"] + assert cast(Any, generator).items == [] + + langfuse_memory_client.flush() + + root_span = _finished_spans_by_name(memory_exporter, "root")[0] + child_spans = _finished_spans_by_name(memory_exporter, "child_step") + + assert len(child_spans) == 2 + assert all(child.parent is not None for child in child_spans) + assert all( + child.parent.span_id == root_span.context.span_id for child in child_spans + ) + assert all( + child.context.trace_id == root_span.context.trace_id for child in child_spans + ) + assert LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT not in root_span.attributes + + +@pytest.mark.asyncio +@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher") +async def test_streaming_response_preserves_context_without_output_capture( + langfuse_memory_client: Any, memory_exporter: Any +) -> None: + class StreamingResponse: + def __init__(self, body_iterator: AsyncGenerator[str, None]) -> None: + self.body_iterator = body_iterator + + @observe(name="stream_step") + async def stream_step(index: int) -> str: + return f"chunk_{index}" + + async def body() -> AsyncGenerator[str, None]: + for index in range(2): + yield await stream_step(index) + + @observe(name="endpoint", capture_output=False) + async def endpoint() -> StreamingResponse: + return StreamingResponse(body()) + + response = await endpoint() + + assert memory_exporter.get_finished_spans() == [] + + assert [item async for item in response.body_iterator] == ["chunk_0", "chunk_1"] + assert cast(Any, response.body_iterator).items == [] + + langfuse_memory_client.flush() + + endpoint_span = _finished_spans_by_name(memory_exporter, "endpoint")[0] + step_spans = _finished_spans_by_name(memory_exporter, "stream_step") + + assert len(step_spans) == 2 + assert all(step.parent is not None for step in step_spans) + assert all( + step.parent.span_id == endpoint_span.context.span_id for step in step_spans + ) + assert all( + step.context.trace_id == endpoint_span.context.trace_id for step in step_spans + ) + assert LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT not in endpoint_span.attributes + + +def test_sync_generator_wrapper_close_ends_span_without_exhaustion() -> None: + def generator() -> Generator[str, None, None]: + yield "item_0" + yield "item_1" + + span = SpanRecorder() + wrapper = _ContextPreservedSyncGeneratorWrapper( + generator(), + contextvars.copy_context(), + cast(Any, span), + False, + None, + ) + + assert next(wrapper) == "item_0" + + wrapper.close() + wrapper.close() + + assert span.ended == 1 + assert span.updates == [] + + +def test_sync_generator_wrapper_close_preserves_context() -> None: + marker = contextvars.ContextVar("marker", default="ambient") + seen: list[str] = [] + + def generator() -> Generator[str, None, None]: + try: + yield "item_0" + yield "item_1" + finally: + seen.append(marker.get()) + + span = SpanRecorder() + context = contextvars.copy_context() + context.run(marker.set, "preserved") + wrapper = _ContextPreservedSyncGeneratorWrapper( + generator(), + context, + cast(Any, span), + False, + None, + ) + + assert next(wrapper) == "item_0" + marker.set("ambient-now") + + wrapper.close() + + assert seen == ["preserved"] + assert span.ended == 1 + + +def test_sync_generator_wrapper_del_ends_span_when_abandoned() -> None: + def generator() -> Generator[str, None, None]: + yield "item_0" + yield "item_1" + + span = SpanRecorder() + wrapper = _ContextPreservedSyncGeneratorWrapper( + generator(), + contextvars.copy_context(), + cast(Any, span), + False, + None, + ) + + assert next(wrapper) == "item_0" + + del wrapper + gc.collect() + + assert span.ended == 1 + assert span.updates == [] + + +@pytest.mark.asyncio +async def test_async_generator_wrapper_aclose_ends_span_without_exhaustion() -> None: + async def generator() -> AsyncGenerator[str, None]: + yield "item_0" + yield "item_1" + + span = SpanRecorder() + wrapper = _ContextPreservedAsyncGeneratorWrapper( + generator(), + contextvars.copy_context(), + cast(Any, span), + False, + None, + ) + + assert await wrapper.__anext__() == "item_0" + + await wrapper.aclose() + await wrapper.close() + + assert span.ended == 1 + assert span.updates == [] + + +@pytest.mark.asyncio +async def test_async_generator_wrapper_aclose_preserves_context() -> None: + marker = contextvars.ContextVar("marker", default="ambient") + seen: list[str] = [] + + async def generator() -> AsyncGenerator[str, None]: + try: + yield "item_0" + yield "item_1" + finally: + seen.append(marker.get()) + + span = SpanRecorder() + context = contextvars.copy_context() + context.run(marker.set, "preserved") + wrapper = _ContextPreservedAsyncGeneratorWrapper( + generator(), + context, + cast(Any, span), + False, + None, + ) + + assert await wrapper.__anext__() == "item_0" + marker.set("ambient-now") + + await wrapper.aclose() + + assert seen == ["preserved"] + assert span.ended == 1 + + +@pytest.mark.asyncio +async def test_async_generator_wrapper_fallback_preserves_context( + monkeypatch: pytest.MonkeyPatch, +) -> None: + marker = contextvars.ContextVar("marker", default="ambient") + seen: list[str] = [] + original_create_task = asyncio.create_task + + def create_task_with_type_error(*args: Any, **kwargs: Any) -> asyncio.Task[Any]: + if "context" in kwargs: + raise TypeError("context argument unsupported") + + return original_create_task(*args, **kwargs) + + monkeypatch.setattr(asyncio, "create_task", create_task_with_type_error) + + async def generator() -> AsyncGenerator[str, None]: + try: + yield marker.get() + yield "item_1" + finally: + seen.append(marker.get()) + + span = SpanRecorder() + context = contextvars.copy_context() + context.run(marker.set, "preserved") + wrapper = _ContextPreservedAsyncGeneratorWrapper( + generator(), + context, + cast(Any, span), + False, + None, + ) + + assert await wrapper.__anext__() == "preserved" + marker.set("ambient-now") + + await wrapper.aclose() + + assert seen == ["preserved"] + assert span.ended == 1 + + +@pytest.mark.asyncio +async def test_async_generator_wrapper_del_ends_span_when_abandoned() -> None: + async def generator() -> AsyncGenerator[str, None]: + yield "item_0" + yield "item_1" + + span = SpanRecorder() + wrapper = _ContextPreservedAsyncGeneratorWrapper( + generator(), + contextvars.copy_context(), + cast(Any, span), + False, + None, + ) + + assert await wrapper.__anext__() == "item_0" + + del wrapper + gc.collect() + await asyncio.sleep(0) + + assert span.ended == 1 + assert span.updates == [] diff --git a/tests/unit/test_openai.py b/tests/unit/test_openai.py new file mode 100644 index 000000000..72923f425 --- /dev/null +++ b/tests/unit/test_openai.py @@ -0,0 +1,744 @@ +import asyncio +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +import langfuse.openai as lf_openai_module +from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse.openai import openai as lf_openai + + +class DummySyncResponse: + def __init__(self) -> None: + self.closed = False + + def close(self) -> None: + self.closed = True + + +class DummyAsyncResponse: + def __init__(self) -> None: + self.closed = False + + async def aclose(self) -> None: + self.closed = True + + +class DummyOpenAIStream(lf_openai.Stream): + def __init__(self, items, response) -> None: + self.response = response + self._iterator = iter(items) + + +class DummyOpenAIAsyncStream(lf_openai.AsyncStream): + def __init__(self, items, response) -> None: + self.response = response + self._iterator = self._stream(items) + + async def _stream(self, items): + for item in items: + yield item + + +class DummyGeneration: + def __init__(self) -> None: + self.end_calls = 0 + + def update(self, **kwargs): + return self + + def end(self) -> None: + self.end_calls += 1 + + +class DummyFallbackAsyncResponse: + def __init__(self) -> None: + self.close_calls = 0 + self.aclose_calls = 0 + + async def close(self) -> None: + self.close_calls += 1 + + async def aclose(self) -> None: + self.aclose_calls += 1 + + +def _make_chat_stream_chunks(): + usage = SimpleNamespace(prompt_tokens=3, completion_tokens=1, total_tokens=4) + + return [ + SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + role="assistant", + content="2", + function_call=None, + tool_calls=None, + ), + finish_reason=None, + ) + ], + usage=None, + ), + SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + role=None, + content=None, + function_call=None, + tool_calls=None, + ), + finish_reason="stop", + ) + ], + usage=usage, + ), + ] + + +def _make_chat_stream_chunks_with_trailing_content_filter_chunk(): + usage = SimpleNamespace(prompt_tokens=3, completion_tokens=1, total_tokens=4) + + return [ + SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + role="assistant", + content="2", + function_call=None, + tool_calls=None, + ), + finish_reason=None, + ) + ], + usage=None, + ), + SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + role=None, + content=None, + function_call=None, + tool_calls=None, + ), + finish_reason="stop", + ) + ], + usage=usage, + ), + SimpleNamespace( + model="", + choices=[ + SimpleNamespace( + delta=None, + finish_reason=None, + content_filter_offsets={ + "check_offset": 44, + "start_offset": 44, + "end_offset": 121, + }, + content_filter_results={ + "hate": {"filtered": False, "severity": "safe"}, + "self_harm": {"filtered": False, "severity": "safe"}, + "sexual": {"filtered": False, "severity": "safe"}, + "violence": {"filtered": False, "severity": "safe"}, + }, + ) + ], + usage=None, + ), + ] + + +def _make_single_chunk_stream(): + return SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + role="assistant", + content="2", + function_call=None, + tool_calls=None, + ), + finish_reason="stop", + ) + ], + usage=None, + ) + + +def test_chat_completion_exports_generation_span( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.OpenAI(api_key="test") + response = SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + message=SimpleNamespace( + role="assistant", + content="2", + function_call=None, + tool_calls=None, + audio=None, + ) + ) + ], + usage=SimpleNamespace(prompt_tokens=3, completion_tokens=1, total_tokens=4), + ) + + with patch.object(openai_client.chat.completions, "_post", return_value=response): + result = openai_client.chat.completions.create( + name="unit-openai-chat", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "1 + 1 = ?"}], + temperature=0, + metadata={"suite": "unit"}, + ) + + assert result is response + + langfuse_memory_client.flush() + span = get_span("unit-openai-chat") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] == "generation" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o-mini" + ) + assert span.attributes["langfuse.observation.metadata.suite"] == "unit" + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_INPUT) == [ + {"role": "user", "content": "1 + 1 = ?"} + ] + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT) == { + "role": "assistant", + "content": "2", + } + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_MODEL_PARAMETERS) == { + "temperature": 0, + "max_tokens": "Infinity", + "top_p": 1, + "frequency_penalty": 0, + "presence_penalty": 0, + } + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "prompt_tokens": 3, + "completion_tokens": 1, + "total_tokens": 4, + } + + +def test_streaming_chat_completion_exports_ttft( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.OpenAI(api_key="test") + usage = SimpleNamespace(prompt_tokens=3, completion_tokens=1, total_tokens=4) + + def fake_stream(): + yield SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + role="assistant", + content="2", + function_call=None, + tool_calls=None, + ), + finish_reason=None, + ) + ], + usage=None, + ) + yield SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + role=None, + content=None, + function_call=None, + tool_calls=None, + ), + finish_reason="stop", + ) + ], + usage=usage, + ) + + with patch.object( + openai_client.chat.completions, "_post", return_value=fake_stream() + ): + stream = openai_client.chat.completions.create( + name="unit-openai-stream", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "1 + 1 = ?"}], + temperature=0, + stream=True, + ) + chunks = list(stream) + + assert len(chunks) == 2 + + langfuse_memory_client.flush() + span = get_span("unit-openai-stream") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] + is not None + ) + assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "prompt_tokens": 3, + "completion_tokens": 1, + "total_tokens": 4, + } + + +def test_chat_completion_error_marks_generation_error(langfuse_memory_client, get_span): + openai_client = lf_openai.OpenAI(api_key="test") + + with patch.object( + openai_client.chat.completions, + "_post", + side_effect=RuntimeError("boom"), + ): + with pytest.raises(RuntimeError, match="boom"): + openai_client.chat.completions.create( + name="unit-openai-error", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "explode"}], + temperature=0, + ) + + langfuse_memory_client.flush() + span = get_span("unit-openai-error") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" + assert ( + "boom" in span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + ) + assert LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT not in span.attributes + + +def test_openai_stream_preserves_original_stream_contract( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.OpenAI(api_key="test") + raw_response = DummySyncResponse() + raw_stream = DummyOpenAIStream(_make_chat_stream_chunks(), raw_response) + + with patch.object(openai_client.chat.completions, "_post", return_value=raw_stream): + stream = openai_client.chat.completions.create( + name="unit-openai-native-stream", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "1 + 1 = ?"}], + temperature=0, + stream=True, + ) + + assert stream is raw_stream + assert isinstance(stream, lf_openai.Stream) + assert stream.response is raw_response + + chunks = list(stream) + stream.close() + + assert len(chunks) == 2 + assert raw_response.closed is True + + langfuse_memory_client.flush() + span = get_span("unit-openai-native-stream") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] + is not None + ) + assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "prompt_tokens": 3, + "completion_tokens": 1, + "total_tokens": 4, + } + + +def test_openai_stream_handles_trailing_azure_content_filter_chunk( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.OpenAI(api_key="test") + raw_stream = DummyOpenAIStream( + _make_chat_stream_chunks_with_trailing_content_filter_chunk(), + DummySyncResponse(), + ) + + with patch.object(openai_client.chat.completions, "_post", return_value=raw_stream): + stream = openai_client.chat.completions.create( + name="unit-openai-native-stream-azure-filter", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "1 + 1 = ?"}], + temperature=0, + stream=True, + ) + + chunks = list(stream) + stream.close() + + assert len(chunks) == 3 + + langfuse_memory_client.flush() + span = get_span("unit-openai-native-stream-azure-filter") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" + assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "prompt_tokens": 3, + "completion_tokens": 1, + "total_tokens": 4, + } + + +def test_openai_stream_break_still_finalizes_generation( + langfuse_memory_client, get_span +): + openai_client = lf_openai.OpenAI(api_key="test") + raw_response = DummySyncResponse() + raw_stream = DummyOpenAIStream(_make_chat_stream_chunks(), raw_response) + + with patch.object(openai_client.chat.completions, "_post", return_value=raw_stream): + stream = openai_client.chat.completions.create( + name="unit-openai-native-stream-break", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "1 + 1 = ?"}], + temperature=0, + stream=True, + ) + + for chunk in stream: + assert chunk.choices[0].delta.content == "2" + break + + assert raw_response.closed is False + + langfuse_memory_client.flush() + span = get_span("unit-openai-native-stream-break") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] + is not None + ) + + +@pytest.mark.asyncio +async def test_async_chat_completion_exports_generation_span( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.AsyncOpenAI(api_key="test") + response = SimpleNamespace( + model="gpt-4o-mini", + choices=[ + SimpleNamespace( + message=SimpleNamespace( + role="assistant", + content="async result", + function_call=None, + tool_calls=None, + audio=None, + ) + ) + ], + usage=SimpleNamespace(prompt_tokens=5, completion_tokens=2, total_tokens=7), + ) + + with patch.object(openai_client.chat.completions, "_post", return_value=response): + result = await openai_client.chat.completions.create( + name="unit-openai-async", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "hello"}], + temperature=0, + ) + + assert result is response + + langfuse_memory_client.flush() + span = get_span("unit-openai-async") + + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT) == { + "role": "assistant", + "content": "async result", + } + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "prompt_tokens": 5, + "completion_tokens": 2, + "total_tokens": 7, + } + + +@pytest.mark.asyncio +async def test_openai_async_stream_preserves_original_stream_contract( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.AsyncOpenAI(api_key="test") + raw_response = DummyAsyncResponse() + raw_stream = DummyOpenAIAsyncStream(_make_chat_stream_chunks(), raw_response) + + with patch.object(openai_client.chat.completions, "_post", return_value=raw_stream): + stream = await openai_client.chat.completions.create( + name="unit-openai-native-async-stream", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "1 + 1 = ?"}], + temperature=0, + stream=True, + ) + + assert stream is raw_stream + assert isinstance(stream, lf_openai.AsyncStream) + assert stream.response is raw_response + assert hasattr(stream, "aclose") + + chunks = [] + async for chunk in stream: + chunks.append(chunk) + + await stream.aclose() + + assert len(chunks) == 2 + assert raw_response.closed is True + + langfuse_memory_client.flush() + span = get_span("unit-openai-native-async-stream") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] + is not None + ) + assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "prompt_tokens": 3, + "completion_tokens": 1, + "total_tokens": 4, + } + + +@pytest.mark.asyncio +async def test_openai_async_stream_supports_anext( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.AsyncOpenAI(api_key="test") + raw_stream = DummyOpenAIAsyncStream( + _make_chat_stream_chunks(), DummyAsyncResponse() + ) + + with patch.object(openai_client.chat.completions, "_post", return_value=raw_stream): + stream = await openai_client.chat.completions.create( + name="unit-openai-native-async-anext", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "1 + 1 = ?"}], + temperature=0, + stream=True, + ) + + first = await stream.__anext__() + second = await stream.__anext__() + + assert first.choices[0].delta.content == "2" + assert second.choices[0].finish_reason == "stop" + + with pytest.raises(StopAsyncIteration): + await stream.__anext__() + + langfuse_memory_client.flush() + span = get_span("unit-openai-native-async-anext") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] + is not None + ) + assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "prompt_tokens": 3, + "completion_tokens": 1, + "total_tokens": 4, + } + + +@pytest.mark.asyncio +async def test_openai_async_stream_break_still_finalizes_generation( + langfuse_memory_client, get_span +): + openai_client = lf_openai.AsyncOpenAI(api_key="test") + raw_stream = DummyOpenAIAsyncStream( + _make_chat_stream_chunks(), DummyAsyncResponse() + ) + + with patch.object(openai_client.chat.completions, "_post", return_value=raw_stream): + stream = await openai_client.chat.completions.create( + name="unit-openai-native-async-stream-break", + model="gpt-4o-mini", + messages=[{"role": "user", "content": "1 + 1 = ?"}], + temperature=0, + stream=True, + ) + + async for chunk in stream: + assert chunk.choices[0].delta.content == "2" + break + + # Async generator finalizers are scheduled across event-loop turns. + for _ in range(5): + await asyncio.sleep(0) + + langfuse_memory_client.flush() + span = get_span("unit-openai-native-async-stream-break") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] + is not None + ) + + +def test_fallback_sync_stream_finalizes_once(): + resource = SimpleNamespace(object="Completions", type="chat") + generation = DummyGeneration() + + def fallback_stream(): + yield _make_single_chunk_stream() + + wrapper = lf_openai_module.LangfuseResponseGeneratorSync( + resource=resource, + response=fallback_stream(), + generation=generation, + ) + + list(wrapper) + + with pytest.raises(StopIteration): + next(wrapper) + + assert generation.end_calls == 1 + + +def test_fallback_sync_stream_exit_finalizes_once(): + resource = SimpleNamespace(object="Completions", type="chat") + generation = DummyGeneration() + + def fallback_stream(): + yield _make_single_chunk_stream() + + wrapper = lf_openai_module.LangfuseResponseGeneratorSync( + resource=resource, + response=fallback_stream(), + generation=generation, + ) + + wrapper.__exit__(None, None, None) + + assert generation.end_calls == 1 + + +@pytest.mark.asyncio +async def test_fallback_async_stream_finalizes_once(): + resource = SimpleNamespace(object="Completions", type="chat") + generation = DummyGeneration() + + async def fallback_stream(): + yield _make_single_chunk_stream() + + wrapper = lf_openai_module.LangfuseResponseGeneratorAsync( + resource=resource, + response=fallback_stream(), + generation=generation, + ) + + async for _ in wrapper: + pass + + with pytest.raises(StopAsyncIteration): + await wrapper.__anext__() + + assert generation.end_calls == 1 + + +@pytest.mark.asyncio +async def test_fallback_async_stream_close_and_exit_finalize_once(): + resource = SimpleNamespace(object="Completions", type="chat") + generation = DummyGeneration() + response = DummyFallbackAsyncResponse() + + wrapper = lf_openai_module.LangfuseResponseGeneratorAsync( + resource=resource, + response=response, + generation=generation, + ) + + await wrapper.close() + await wrapper.__aexit__(None, None, None) + + assert generation.end_calls == 1 + assert response.close_calls == 1 + assert response.aclose_calls == 1 + + +@pytest.mark.asyncio +async def test_fallback_async_stream_aclose_finalizes_once(): + resource = SimpleNamespace(object="Completions", type="chat") + generation = DummyGeneration() + + async def fallback_stream(): + yield _make_single_chunk_stream() + + wrapper = lf_openai_module.LangfuseResponseGeneratorAsync( + resource=resource, + response=fallback_stream(), + generation=generation, + ) + + await wrapper.aclose() + + assert generation.end_calls == 1 + + +def test_embedding_exports_dimensions_and_count( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.OpenAI(api_key="test") + response = SimpleNamespace( + model="text-embedding-3-small", + data=[SimpleNamespace(embedding=[0.1, 0.2, 0.3])], + usage=SimpleNamespace(prompt_tokens=2, total_tokens=2), + ) + + with patch.object(openai_client.embeddings, "_post", return_value=response): + result = openai_client.embeddings.create( + name="unit-openai-embedding", + model="text-embedding-3-small", + input="hello world", + ) + + assert result is response + + langfuse_memory_client.flush() + span = get_span("unit-openai-embedding") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] == "embedding" + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT) == { + "dimensions": 3, + "count": 1, + } + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "input": 2 + } diff --git a/tests/unit/test_openai_prompt_extraction.py b/tests/unit/test_openai_prompt_extraction.py new file mode 100644 index 000000000..9d1ab02cc --- /dev/null +++ b/tests/unit/test_openai_prompt_extraction.py @@ -0,0 +1,46 @@ +import pytest + +try: + # Compatibility across OpenAI SDK versions where NOT_GIVEN export moved. + from openai import NOT_GIVEN +except ImportError: + from openai._types import NOT_GIVEN + +from langfuse.openai import _extract_responses_prompt + + +@pytest.mark.parametrize( + "kwargs, expected", + [ + ({"input": "Hello!"}, "Hello!"), + ( + {"instructions": "You are helpful.", "input": "Hello!"}, + [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello!"}, + ], + ), + ( + { + "instructions": "You are helpful.", + "input": [{"role": "user", "content": "Hello!"}], + }, + [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello!"}, + ], + ), + ( + {"instructions": "You are helpful."}, + {"instructions": "You are helpful."}, + ), + ( + {"instructions": "You are helpful.", "input": NOT_GIVEN}, + {"instructions": "You are helpful."}, + ), + ({"instructions": NOT_GIVEN, "input": "Hello!"}, "Hello!"), + ({"instructions": NOT_GIVEN, "input": NOT_GIVEN}, None), + ], +) +def test_extract_responses_prompt(kwargs, expected): + assert _extract_responses_prompt(kwargs) == expected diff --git a/tests/test_otel.py b/tests/unit/test_otel.py similarity index 71% rename from tests/test_otel.py rename to tests/unit/test_otel.py index 3e52b10fc..f752df607 100644 --- a/tests/test_otel.py +++ b/tests/unit/test_otel.py @@ -14,6 +14,7 @@ ) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from langfuse import propagate_attributes from langfuse._client.attributes import LangfuseOtelSpanAttributes from langfuse._client.client import Langfuse from langfuse._client.resource_manager import LangfuseResourceManager @@ -53,10 +54,17 @@ class TestOTelBase: @pytest.fixture(scope="function", autouse=True) def cleanup_otel(self): """Reset OpenTelemetry state between tests.""" - original_provider = trace_api.get_tracer_provider() + from opentelemetry.util._once import Once + + trace_api._TRACER_PROVIDER = None + trace_api._PROXY_TRACER_PROVIDER = trace_api.ProxyTracerProvider() + trace_api._TRACER_PROVIDER_SET_ONCE = Once() + yield - trace_api.set_tracer_provider(original_provider) LangfuseResourceManager.reset() + trace_api._TRACER_PROVIDER = None + trace_api._PROXY_TRACER_PROVIDER = trace_api.ProxyTracerProvider() + trace_api._TRACER_PROVIDER_SET_ONCE = Once() @pytest.fixture def memory_exporter(self): @@ -80,28 +88,37 @@ def mock_processor_init(self, monkeypatch, memory_exporter): """Mock the LangfuseSpanProcessor initialization to avoid HTTP traffic.""" def mock_init(self, **kwargs): + import threading + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from langfuse._client.span_filter import is_default_export_span + self.public_key = kwargs.get("public_key", "test-key") blocked_scopes = kwargs.get("blocked_instrumentation_scopes") self.blocked_instrumentation_scopes = ( blocked_scopes if blocked_scopes is not None else [] ) + self._should_export_span = ( + kwargs.get("should_export_span") or is_default_export_span + ) + self._app_root_lock = threading.Lock() + self._span_export_expectation_by_id = {} BatchSpanProcessor.__init__( self, span_exporter=memory_exporter, max_export_batch_size=512, - schedule_delay_millis=5000, + schedule_delay_millis=1, ) monkeypatch.setattr( - "langfuse._client.span_processor.LangfuseSpanProcessor.__init__", mock_init + "langfuse._client.span_processor.LangfuseSpanProcessor.__init__", + mock_init, ) @pytest.fixture def langfuse_client(self, monkeypatch, tracer_provider, mock_processor_init): """Create a mocked Langfuse client for testing.""" - # Set environment variables monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "test-public-key") monkeypatch.setenv("LANGFUSE_SECRET_KEY", "test-secret-key") @@ -110,7 +127,7 @@ def langfuse_client(self, monkeypatch, tracer_provider, mock_processor_init): client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", tracing_enabled=True, ) @@ -134,7 +151,7 @@ def _create_client(**kwargs): client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", tracing_enabled=True, **kwargs, ) @@ -207,14 +224,14 @@ def verify_span_attribute( ): """Verify that a span has a specific attribute with an optional expected value.""" attributes = span_data["attributes"] - assert ( - attribute_key in attributes - ), f"Attribute {attribute_key} not found in span" + assert attribute_key in attributes, ( + f"Attribute {attribute_key} not found in span" + ) if expected_value is not None: - assert ( - attributes[attribute_key] == expected_value - ), f"Expected {attribute_key} to be {expected_value}, got {attributes[attribute_key]}" + assert attributes[attribute_key] == expected_value, ( + f"Expected {attribute_key} to be {expected_value}, got {attributes[attribute_key]}" + ) return attributes[attribute_key] @@ -226,20 +243,20 @@ def verify_json_attribute( parsed_json = json.loads(json_string) if expected_dict is not None: - assert ( - parsed_json == expected_dict - ), f"Expected JSON {attribute_key} to be {expected_dict}, got {parsed_json}" + assert parsed_json == expected_dict, ( + f"Expected JSON {attribute_key} to be {expected_dict}, got {parsed_json}" + ) return parsed_json def assert_parent_child_relationship(self, parent_span: dict, child_span: dict): """Verify parent-child relationship between two spans.""" - assert ( - child_span["parent_span_id"] == parent_span["span_id"] - ), f"Child span {child_span['name']} should have parent {parent_span['name']}" - assert ( - child_span["trace_id"] == parent_span["trace_id"] - ), f"Child span {child_span['name']} should have same trace ID as parent {parent_span['name']}" + assert child_span["parent_span_id"] == parent_span["span_id"], ( + f"Child span {child_span['name']} should have parent {parent_span['name']}" + ) + assert child_span["trace_id"] == parent_span["trace_id"], ( + f"Child span {child_span['name']} should have same trace ID as parent {parent_span['name']}" + ) class TestBasicSpans(TestOTelBase): @@ -248,16 +265,18 @@ class TestBasicSpans(TestOTelBase): def test_basic_span_creation(self, langfuse_client, memory_exporter): """Test that a basic span can be created with attributes.""" # Create a span and end it - span = langfuse_client.start_span(name="test-span", input={"test": "value"}) + span = langfuse_client.start_observation( + name="test-span", input={"test": "value"} + ) span.end() # Get spans with our name spans = self.get_spans_by_name(memory_exporter, "test-span") # Verify we created exactly one span - assert ( - len(spans) == 1 - ), f"Expected 1 span named 'test-span', but found {len(spans)}" + assert len(spans) == 1, ( + f"Expected 1 span named 'test-span', but found {len(spans)}" + ) span_data = spans[0] # Verify the span attributes @@ -273,15 +292,19 @@ def test_basic_span_creation(self, langfuse_client, memory_exporter): def test_span_hierarchy(self, langfuse_client, memory_exporter): """Test creating nested spans and verify their parent-child relationships.""" # Create parent span - with langfuse_client.start_as_current_span(name="parent-span") as parent_span: + with langfuse_client.start_as_current_observation( + name="parent-span" + ) as parent_span: # Create a child span - child_span = parent_span.start_span(name="child-span") + child_span = parent_span.start_observation(name="child-span") child_span.end() # Create another child span using context manager - with parent_span.start_as_current_span(name="child-span-2") as child_span_2: + with parent_span.start_as_current_observation( + name="child-span-2" + ) as child_span_2: # Create a grandchild span - grandchild = child_span_2.start_span(name="grandchild-span") + grandchild = child_span_2.start_observation(name="grandchild-span") grandchild.end() # Get all spans @@ -312,7 +335,7 @@ def test_span_hierarchy(self, langfuse_client, memory_exporter): def test_update_current_span_name(self, langfuse_client, memory_exporter): """Test updating current span name via update_current_span method.""" # Create a span using context manager - with langfuse_client.start_as_current_span(name="original-current-span"): + with langfuse_client.start_as_current_observation(name="original-current-span"): # Update the current span name langfuse_client.update_current_span(name="updated-current-span") @@ -329,7 +352,7 @@ def test_update_current_span_name(self, langfuse_client, memory_exporter): def test_span_attributes(self, langfuse_client, memory_exporter): """Test that span attributes are correctly set and updated.""" # Create a span with attributes - span = langfuse_client.start_span( + span = langfuse_client.start_observation( name="attribute-span", input={"prompt": "Test prompt"}, output={"response": "Test response"}, @@ -380,7 +403,7 @@ def test_span_attributes(self, langfuse_client, memory_exporter): def test_span_name_update(self, langfuse_client, memory_exporter): """Test updating span name via update method.""" # Create a span with initial name - span = langfuse_client.start_span(name="original-span-name") + span = langfuse_client.start_observation(name="original-span-name") # Update the span name span.update(name="updated-span-name") @@ -397,7 +420,8 @@ def test_span_name_update(self, langfuse_client, memory_exporter): def test_generation_span(self, langfuse_client, memory_exporter): """Test creating a generation span with model-specific attributes.""" # Create a generation - generation = langfuse_client.start_generation( + generation = langfuse_client.start_observation( + as_type="generation", name="test-generation", model="gpt-4", model_parameters={"temperature": 0.7, "max_tokens": 100}, @@ -431,8 +455,10 @@ def test_generation_span(self, langfuse_client, memory_exporter): def test_generation_name_update(self, langfuse_client, memory_exporter): """Test updating generation name via update method.""" # Create a generation with initial name - generation = langfuse_client.start_generation( - name="original-generation-name", model="gpt-4" + generation = langfuse_client.start_observation( + as_type="generation", + name="original-generation-name", + model="gpt-4", ) # Update the generation name @@ -451,16 +477,16 @@ def test_generation_name_update(self, langfuse_client, memory_exporter): def test_trace_update(self, langfuse_client, memory_exporter): """Test updating trace level attributes.""" - # Create a span and update trace attributes - with langfuse_client.start_as_current_span(name="trace-span") as span: - span.update_trace( - name="updated-trace-name", + # Create a span and set trace attributes using propagate_attributes and set_trace_io + with langfuse_client.start_as_current_observation(name="trace-span") as span: + with propagate_attributes( + trace_name="updated-trace-name", user_id="test-user", session_id="test-session", tags=["tag1", "tag2"], - input={"trace-input": "value"}, metadata={"trace-meta": "data"}, - ) + ): + span.set_trace_io(input={"trace-input": "value"}) # Get the span data spans = self.get_spans_by_name(memory_exporter, "trace-span") @@ -490,34 +516,40 @@ def test_trace_update(self, langfuse_client, memory_exporter): def test_complex_scenario(self, langfuse_client, memory_exporter): """Test a more complex scenario with multiple operations and nesting.""" # Create a trace with a main span - with langfuse_client.start_as_current_span(name="main-flow") as main_span: + with langfuse_client.start_as_current_observation( + name="main-flow" + ) as main_span: # Add trace information - main_span.update_trace( - name="complex-test", + with propagate_attributes( + trace_name="complex-test", user_id="complex-user", session_id="complex-session", - ) - - # Add a processing span - with main_span.start_as_current_span(name="processing") as processing: - processing.update(metadata={"step": "processing"}) - - # Add an LLM generation - with main_span.start_as_current_generation( - name="llm-call", - model="gpt-3.5-turbo", - input={"prompt": "Summarize this text"}, - metadata={"service": "OpenAI"}, - ) as generation: - # Update the generation with results - generation.update( - output={"text": "This is a summary"}, - usage_details={"input": 20, "output": 5, "total": 25}, - ) - - # Final processing step - with main_span.start_as_current_span(name="post-processing") as post_proc: - post_proc.update(metadata={"step": "post-processing"}) + ): + # Add a processing span + with main_span.start_as_current_observation( + name="processing" + ) as processing: + processing.update(metadata={"step": "processing"}) + + # Add an LLM generation + with main_span.start_as_current_observation( + as_type="generation", + name="llm-call", + model="gpt-3.5-turbo", + input={"prompt": "Summarize this text"}, + metadata={"service": "OpenAI"}, + ) as generation: + # Update the generation with results + generation.update( + output={"text": "This is a summary"}, + usage_details={"input": 20, "output": 5, "total": 25}, + ) + + # Final processing step + with main_span.start_as_current_observation( + name="post-processing" + ) as post_proc: + post_proc.update(metadata={"step": "post-processing"}) # Get all spans spans = [ @@ -572,8 +604,10 @@ def test_complex_scenario(self, langfuse_client, memory_exporter): def test_update_current_generation_name(self, langfuse_client, memory_exporter): """Test updating current generation name via update_current_generation method.""" # Create a generation using context manager - with langfuse_client.start_as_current_generation( - name="original-current-generation", model="gpt-4" + with langfuse_client.start_as_current_observation( + as_type="generation", + name="original-current-generation", + model="gpt-4", ): # Update the current generation name langfuse_client.update_current_generation(name="updated-current-generation") @@ -588,6 +622,185 @@ def test_update_current_generation_name(self, langfuse_client, memory_exporter): ) assert len(original_spans) == 0, "Expected no generations with original name" + def test_start_as_current_observation_types(self, langfuse_client, memory_exporter): + """Test creating different observation types using start_as_current_observation.""" + # Test each observation type from ObservationTypeLiteralNoEvent + observation_types = [ + "span", + "generation", + "agent", + "tool", + "chain", + "retriever", + "evaluator", + "embedding", + "guardrail", + ] + + for obs_type in observation_types: + with langfuse_client.start_as_current_observation( + name=f"test-{obs_type}", as_type=obs_type + ): + with propagate_attributes(trace_name=f"trace-{obs_type}"): + pass + + spans = [ + self.get_span_data(span) for span in memory_exporter.get_finished_spans() + ] + + # Find spans by name and verify their observation types + for obs_type in observation_types: + expected_name = f"test-{obs_type}" + matching_spans = [span for span in spans if span["name"] == expected_name] + assert len(matching_spans) == 1, ( + f"Expected one span with name {expected_name}" + ) + + span_data = matching_spans[0] + expected_otel_type = obs_type # OTEL attributes use lowercase + actual_type = span_data["attributes"].get( + LangfuseOtelSpanAttributes.OBSERVATION_TYPE + ) + + assert actual_type == expected_otel_type, ( + f"Expected observation type {expected_otel_type}, got {actual_type}" + ) + + def test_start_observation(self, langfuse_client, memory_exporter): + """Test creating different observation types using start_observation.""" + from langfuse._client.constants import ( + ObservationTypeGenerationLike, + ObservationTypeLiteral, + get_observation_types_list, + ) + + # Test each observation type defined in constants - this ensures we test all supported types + observation_types = get_observation_types_list(ObservationTypeLiteral) + + # Create a main span to use for child creation + with langfuse_client.start_as_current_observation( + name="factory-test-parent" + ) as parent_span: + created_observations = [] + + for obs_type in observation_types: + if obs_type in get_observation_types_list( + ObservationTypeGenerationLike + ): + # Generation-like types with extra parameters + obs = parent_span.start_observation( + name=f"factory-{obs_type}", + as_type=obs_type, + input={"test": f"{obs_type}_input"}, + model="test-model", + model_parameters={"temperature": 0.7}, + usage_details={"input": 10, "output": 20}, + ) + if obs_type != "event": # Events are auto-ended + obs.end() + created_observations.append((obs_type, obs)) + elif obs_type == "event": + # Test event creation through start_observation (should be auto-ended) + obs = parent_span.start_observation( + name=f"factory-{obs_type}", + as_type=obs_type, + input={"test": f"{obs_type}_input"}, + ) + created_observations.append((obs_type, obs)) + else: + # Span-like types (span, guardrail) + obs = parent_span.start_observation( + name=f"factory-{obs_type}", + as_type=obs_type, + input={"test": f"{obs_type}_input"}, + ) + obs.end() + created_observations.append((obs_type, obs)) + + spans = [ + self.get_span_data(span) for span in memory_exporter.get_finished_spans() + ] + + # Verify factory pattern created correct observation types + for obs_type in observation_types: + expected_name = f"factory-{obs_type}" + matching_spans = [span for span in spans if span["name"] == expected_name] + assert len(matching_spans) == 1, ( + f"Expected one span with name {expected_name}, found {len(matching_spans)}" + ) + + span_data = matching_spans[0] + actual_type = span_data["attributes"].get( + LangfuseOtelSpanAttributes.OBSERVATION_TYPE + ) + + assert actual_type == obs_type, ( + f"Factory pattern failed: Expected observation type {obs_type}, got {actual_type}" + ) + + # Ensure returned objects are of correct types + for obs_type, obs_instance in created_observations: + if obs_type == "span": + from langfuse._client.span import LangfuseSpan + + assert isinstance(obs_instance, LangfuseSpan), ( + f"Expected LangfuseSpan, got {type(obs_instance)}" + ) + elif obs_type == "generation": + from langfuse._client.span import LangfuseGeneration + + assert isinstance(obs_instance, LangfuseGeneration), ( + f"Expected LangfuseGeneration, got {type(obs_instance)}" + ) + elif obs_type == "agent": + from langfuse._client.span import LangfuseAgent + + assert isinstance(obs_instance, LangfuseAgent), ( + f"Expected LangfuseAgent, got {type(obs_instance)}" + ) + elif obs_type == "tool": + from langfuse._client.span import LangfuseTool + + assert isinstance(obs_instance, LangfuseTool), ( + f"Expected LangfuseTool, got {type(obs_instance)}" + ) + elif obs_type == "chain": + from langfuse._client.span import LangfuseChain + + assert isinstance(obs_instance, LangfuseChain), ( + f"Expected LangfuseChain, got {type(obs_instance)}" + ) + elif obs_type == "retriever": + from langfuse._client.span import LangfuseRetriever + + assert isinstance(obs_instance, LangfuseRetriever), ( + f"Expected LangfuseRetriever, got {type(obs_instance)}" + ) + elif obs_type == "evaluator": + from langfuse._client.span import LangfuseEvaluator + + assert isinstance(obs_instance, LangfuseEvaluator), ( + f"Expected LangfuseEvaluator, got {type(obs_instance)}" + ) + elif obs_type == "embedding": + from langfuse._client.span import LangfuseEmbedding + + assert isinstance(obs_instance, LangfuseEmbedding), ( + f"Expected LangfuseEmbedding, got {type(obs_instance)}" + ) + elif obs_type == "guardrail": + from langfuse._client.span import LangfuseGuardrail + + assert isinstance(obs_instance, LangfuseGuardrail), ( + f"Expected LangfuseGuardrail, got {type(obs_instance)}" + ) + elif obs_type == "event": + from langfuse._client.span import LangfuseEvent + + assert isinstance(obs_instance, LangfuseEvent), ( + f"Expected LangfuseEvent, got {type(obs_instance)}" + ) + def test_custom_trace_id(self, langfuse_client, memory_exporter): """Test setting a custom trace ID.""" # Create a custom trace ID @@ -595,7 +808,7 @@ def test_custom_trace_id(self, langfuse_client, memory_exporter): # Create a span with this custom trace ID using trace_context trace_context = {"trace_id": custom_trace_id} - span = langfuse_client.start_span( + span = langfuse_client.start_observation( name="custom-trace-span", trace_context=trace_context, input={"test": "value"}, @@ -607,13 +820,13 @@ def test_custom_trace_id(self, langfuse_client, memory_exporter): assert len(spans) == 1, "Expected one span" span_data = spans[0] - assert ( - span_data["trace_id"] == custom_trace_id - ), "Trace ID doesn't match custom ID" + assert span_data["trace_id"] == custom_trace_id, ( + "Trace ID doesn't match custom ID" + ) assert span_data["attributes"][LangfuseOtelSpanAttributes.AS_ROOT] is True # Test additional spans with the same trace context - child_span = langfuse_client.start_span( + child_span = langfuse_client.start_observation( name="child-span", trace_context=trace_context, input={"child": "data"} ) child_span.end() @@ -621,9 +834,9 @@ def test_custom_trace_id(self, langfuse_client, memory_exporter): # Verify child span uses the same trace ID child_spans = self.get_spans_by_name(memory_exporter, "child-span") assert len(child_spans) == 1, "Expected one child span" - assert ( - child_spans[0]["trace_id"] == custom_trace_id - ), "Child span has wrong trace ID" + assert child_spans[0]["trace_id"] == custom_trace_id, ( + "Child span has wrong trace ID" + ) def test_custom_parent_span_id(self, langfuse_client, memory_exporter): """Test setting a custom parent span ID.""" @@ -635,7 +848,7 @@ def test_custom_parent_span_id(self, langfuse_client, memory_exporter): trace_context = {"trace_id": trace_id, "parent_span_id": parent_span_id} # Create a span with this context - span = langfuse_client.start_span( + span = langfuse_client.start_observation( name="custom-parent-span", trace_context=trace_context ) span.end() @@ -649,9 +862,12 @@ def test_custom_parent_span_id(self, langfuse_client, memory_exporter): def test_multiple_generations_in_trace(self, langfuse_client, memory_exporter): """Test creating multiple generation spans within the same trace.""" # Create a trace with multiple generation spans - with langfuse_client.start_as_current_span(name="multi-gen-flow") as main_span: + with langfuse_client.start_as_current_observation( + name="multi-gen-flow" + ) as main_span: # First generation - gen1 = main_span.start_generation( + gen1 = main_span.start_observation( + as_type="generation", name="generation-1", model="gpt-3.5-turbo", input={"prompt": "First prompt"}, @@ -662,7 +878,8 @@ def test_multiple_generations_in_trace(self, langfuse_client, memory_exporter): gen1.end() # Second generation with different model - gen2 = main_span.start_generation( + gen2 = main_span.start_observation( + as_type="generation", name="generation-2", model="gpt-4", input={"prompt": "Second prompt"}, @@ -731,7 +948,7 @@ def test_multiple_generations_in_trace(self, langfuse_client, memory_exporter): def test_error_handling(self, langfuse_client, memory_exporter): """Test error handling in span operations.""" # Create a span that will have an error - span = langfuse_client.start_span(name="error-span") + span = langfuse_client.start_observation(name="error-span") # Set an error status on the span import traceback @@ -766,6 +983,283 @@ def test_error_handling(self, langfuse_client, memory_exporter): == "Test error message" ) + def test_error_level_in_span_creation(self, langfuse_client, memory_exporter): + """Test that OTEL span status is set to ERROR when creating spans with level='ERROR'.""" + # Create a span with level="ERROR" at creation time + span = langfuse_client.start_observation( + name="create-error-span", + level="ERROR", + status_message="Initial error state", + ) + span.end() + + # Get the raw OTEL spans to check the status + raw_spans = [ + s + for s in memory_exporter.get_finished_spans() + if s.name == "create-error-span" + ] + assert len(raw_spans) == 1, "Expected one span" + raw_span = raw_spans[0] + + # Verify OTEL span status was set to ERROR + from opentelemetry.trace.status import StatusCode + + assert raw_span.status.status_code == StatusCode.ERROR + assert raw_span.status.description == "Initial error state" + + # Also verify Langfuse attributes + spans = self.get_spans_by_name(memory_exporter, "create-error-span") + span_data = spans[0] + attributes = span_data["attributes"] + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == "Initial error state" + ) + + def test_error_level_in_span_update(self, langfuse_client, memory_exporter): + """Test that OTEL span status is set to ERROR when updating spans to level='ERROR'.""" + # Create a normal span + span = langfuse_client.start_observation(name="update-error-span", level="INFO") + + # Update it to ERROR level + span.update(level="ERROR", status_message="Updated to error state") + span.end() + + # Get the raw OTEL spans to check the status + raw_spans = [ + s + for s in memory_exporter.get_finished_spans() + if s.name == "update-error-span" + ] + assert len(raw_spans) == 1, "Expected one span" + raw_span = raw_spans[0] + + # Verify OTEL span status was set to ERROR + from opentelemetry.trace.status import StatusCode + + assert raw_span.status.status_code == StatusCode.ERROR + assert raw_span.status.description == "Updated to error state" + + # Also verify Langfuse attributes + spans = self.get_spans_by_name(memory_exporter, "update-error-span") + span_data = spans[0] + attributes = span_data["attributes"] + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == "Updated to error state" + ) + + def test_generation_error_level_in_creation(self, langfuse_client, memory_exporter): + """Test that OTEL span status is set to ERROR when creating generations with level='ERROR'.""" + # Create a generation with level="ERROR" at creation time + generation = langfuse_client.start_observation( + as_type="generation", + name="create-error-generation", + model="gpt-4", + level="ERROR", + status_message="Generation failed during creation", + ) + generation.end() + + # Get the raw OTEL spans to check the status + raw_spans = [ + s + for s in memory_exporter.get_finished_spans() + if s.name == "create-error-generation" + ] + assert len(raw_spans) == 1, "Expected one span" + raw_span = raw_spans[0] + + # Verify OTEL span status was set to ERROR + from opentelemetry.trace.status import StatusCode + + assert raw_span.status.status_code == StatusCode.ERROR + assert raw_span.status.description == "Generation failed during creation" + + # Also verify Langfuse attributes + spans = self.get_spans_by_name(memory_exporter, "create-error-generation") + span_data = spans[0] + attributes = span_data["attributes"] + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == "Generation failed during creation" + ) + + def test_generation_error_level_in_update(self, langfuse_client, memory_exporter): + """Test that OTEL span status is set to ERROR when updating generations to level='ERROR'.""" + # Create a normal generation + generation = langfuse_client.start_observation( + as_type="generation", + name="update-error-generation", + model="gpt-4", + level="INFO", + ) + + # Update it to ERROR level + generation.update( + level="ERROR", status_message="Generation failed during execution" + ) + generation.end() + + # Get the raw OTEL spans to check the status + raw_spans = [ + s + for s in memory_exporter.get_finished_spans() + if s.name == "update-error-generation" + ] + assert len(raw_spans) == 1, "Expected one span" + raw_span = raw_spans[0] + + # Verify OTEL span status was set to ERROR + from opentelemetry.trace.status import StatusCode + + assert raw_span.status.status_code == StatusCode.ERROR + assert raw_span.status.description == "Generation failed during execution" + + # Also verify Langfuse attributes + spans = self.get_spans_by_name(memory_exporter, "update-error-generation") + span_data = spans[0] + attributes = span_data["attributes"] + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == "Generation failed during execution" + ) + + def test_non_error_levels_dont_set_otel_status( + self, langfuse_client, memory_exporter + ): + """Test that non-ERROR levels don't set OTEL span status to ERROR.""" + # Test different non-error levels + test_levels = ["INFO", "WARNING", "DEBUG", None] + + for i, level in enumerate(test_levels): + span_name = f"non-error-span-{i}" + span = langfuse_client.start_observation(name=span_name, level=level) + + # Update with same level to test update path too + if level is not None: + span.update(level=level, status_message="Not an error") + + span.end() + + # Get the raw OTEL spans to check the status + raw_spans = [ + s for s in memory_exporter.get_finished_spans() if s.name == span_name + ] + assert len(raw_spans) == 1, f"Expected one span for {span_name}" + raw_span = raw_spans[0] + + # Verify OTEL span status was NOT set to ERROR + from opentelemetry.trace.status import StatusCode + + # Default status should be UNSET, not ERROR + assert raw_span.status.status_code != StatusCode.ERROR, ( + f"Level {level} should not set ERROR status" + ) + + def test_multiple_error_updates(self, langfuse_client, memory_exporter): + """Test that multiple ERROR level updates work correctly.""" + # Create a span + span = langfuse_client.start_observation(name="multi-error-span") + + # First error update + span.update(level="ERROR", status_message="First error") + + # Second error update - should overwrite the first + span.update(level="ERROR", status_message="Second error") + + span.end() + + # Get the raw OTEL spans to check the status + raw_spans = [ + s + for s in memory_exporter.get_finished_spans() + if s.name == "multi-error-span" + ] + assert len(raw_spans) == 1, "Expected one span" + raw_span = raw_spans[0] + + # Verify OTEL span status shows the last error message + from opentelemetry.trace.status import StatusCode + + assert raw_span.status.status_code == StatusCode.ERROR + assert raw_span.status.description == "Second error" + + def test_error_without_status_message(self, langfuse_client, memory_exporter): + """Test that ERROR level works even without status_message.""" + # Create a span with ERROR level but no status message + span = langfuse_client.start_observation( + name="error-no-message-span", level="ERROR" + ) + span.end() + + # Get the raw OTEL spans to check the status + raw_spans = [ + s + for s in memory_exporter.get_finished_spans() + if s.name == "error-no-message-span" + ] + assert len(raw_spans) == 1, "Expected one span" + raw_span = raw_spans[0] + + # Verify OTEL span status was set to ERROR even without description + from opentelemetry.trace.status import StatusCode + + assert raw_span.status.status_code == StatusCode.ERROR + # Description should be None when no status_message provided + assert raw_span.status.description is None + + def test_different_observation_types_error_handling( + self, langfuse_client, memory_exporter + ): + """Test that ERROR level setting works for different observation types.""" + # Test different observation types + observation_types = [ + "agent", + "tool", + "chain", + "retriever", + "evaluator", + "embedding", + "guardrail", + ] + + # Create a parent span for child observations + with langfuse_client.start_as_current_observation( + name="error-test-parent" + ) as parent: + for obs_type in observation_types: + # Create observation with ERROR level + obs = parent.start_observation( + name=f"error-{obs_type}", + as_type=obs_type, + level="ERROR", + status_message=f"{obs_type} failed", + ) + obs.end() + + # Check that all observations have correct OTEL status + raw_spans = memory_exporter.get_finished_spans() + + for obs_type in observation_types: + obs_spans = [s for s in raw_spans if s.name == f"error-{obs_type}"] + assert len(obs_spans) == 1, f"Expected one span for {obs_type}" + + raw_span = obs_spans[0] + from opentelemetry.trace.status import StatusCode + + assert raw_span.status.status_code == StatusCode.ERROR, ( + f"{obs_type} should have ERROR status" + ) + assert raw_span.status.description == f"{obs_type} failed", ( + f"{obs_type} should have correct description" + ) + class TestAdvancedSpans(TestOTelBase): """Tests for advanced span functionality including generations, timing, and usage metrics.""" @@ -805,7 +1299,8 @@ def test_complex_model_parameters(self, langfuse_client, memory_exporter): } # Create a generation with these complex parameters - generation = langfuse_client.start_generation( + generation = langfuse_client.start_observation( + as_type="generation", name="complex-params-test", model="gpt-4", model_parameters=complex_params, @@ -844,7 +1339,8 @@ def test_complex_model_parameters(self, langfuse_client, memory_exporter): def test_updating_current_generation(self, langfuse_client, memory_exporter): """Test that an in-progress generation can be updated multiple times.""" # Create a generation - generation = langfuse_client.start_generation( + generation = langfuse_client.start_observation( + as_type="generation", name="updating-generation", model="gpt-4", input={"prompt": "Write a story about a robot"}, @@ -929,20 +1425,20 @@ def test_sampling(self, monkeypatch, tracer_provider, mock_processor_init): client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", tracing_enabled=True, sample_rate=0, # No sampling ) # Create several spans for i in range(5): - span = client.start_span(name=f"sampled-span-{i}") + span = client.start_observation(name=f"sampled-span-{i}") span.end() # With a sample rate of 0, we should have no spans - assert ( - len(sampled_exporter.get_finished_spans()) == 0 - ), "Expected no spans with 0 sampling" + assert len(sampled_exporter.get_finished_spans()) == 0, ( + "Expected no spans with 0 sampling" + ) # Restore the original provider trace_api.set_tracer_provider(original_provider) @@ -951,7 +1447,7 @@ def test_sampling(self, monkeypatch, tracer_provider, mock_processor_init): def test_shutdown_and_flush(self, langfuse_client, memory_exporter): """Test shutdown and flush operations.""" # Create a span without ending it - span = langfuse_client.start_span(name="flush-test-span") + span = langfuse_client.start_observation(name="flush-test-span") # Explicitly flush langfuse_client.flush() @@ -968,7 +1464,7 @@ def test_shutdown_and_flush(self, langfuse_client, memory_exporter): assert len(spans) == 1, "Span should be exported after ending" # Create another span for shutdown testing - langfuse_client.start_span(name="shutdown-test-span") + langfuse_client.start_observation(name="shutdown-test-span") # Call shutdown (should flush any pending spans) langfuse_client.shutdown() @@ -979,7 +1475,7 @@ def test_disabled_tracing(self, monkeypatch, tracer_provider, mock_processor_ini client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", tracing_enabled=False, ) @@ -989,18 +1485,19 @@ def test_disabled_tracing(self, monkeypatch, tracer_provider, mock_processor_ini tracer_provider.add_span_processor(processor) # Attempt to create spans and trace operations - span = client.start_span(name="disabled-span", input={"key": "value"}) + span = client.start_observation(name="disabled-span", input={"key": "value"}) span.update(output={"result": "test"}) span.end() - with client.start_as_current_span(name="disabled-context-span") as context_span: - context_span.update_trace(name="disabled-trace") + with client.start_as_current_observation(name="disabled-context-span"): + with propagate_attributes(trace_name="disabled-trace"): + pass # Verify no spans were created spans = exporter.get_finished_spans() - assert ( - len(spans) == 0 - ), f"Expected no spans when tracing is disabled, got {len(spans)}" + assert len(spans) == 0, ( + f"Expected no spans when tracing is disabled, got {len(spans)}" + ) def test_trace_id_generation(self, langfuse_client): """Test trace ID generation follows expected format.""" @@ -1009,12 +1506,12 @@ def test_trace_id_generation(self, langfuse_client): trace_id2 = langfuse_client.create_trace_id() # Verify format: 32 hex characters - assert ( - len(trace_id1) == 32 - ), f"Trace ID length should be 32, got {len(trace_id1)}" - assert ( - len(trace_id2) == 32 - ), f"Trace ID length should be 32, got {len(trace_id2)}" + assert len(trace_id1) == 32, ( + f"Trace ID length should be 32, got {len(trace_id1)}" + ) + assert len(trace_id2) == 32, ( + f"Trace ID length should be 32, got {len(trace_id2)}" + ) # jerify it's a valid hex string int(trace_id1, 16), "Trace ID should be a valid hex string" @@ -1384,7 +1881,7 @@ def update_random_metadata(thread_id): update = random.choice(updates) # Sleep a tiny bit to simulate work and increase chances of thread interleaving - time.sleep(random.uniform(0.001, 0.01)) + time.sleep(random.uniform(0.0005, 0.001)) # Apply the update to current_metadata (in a real system, this would update OTEL span) with metadata_lock: @@ -1497,9 +1994,18 @@ def multi_project_setup(self, monkeypatch): # Setup tracers with appropriate project-specific span exporting def mock_processor_init(self, **kwargs): + import threading + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from langfuse._client.span_filter import is_default_export_span + self.public_key = kwargs.get("public_key", "test-key") + self._should_export_span = ( + kwargs.get("should_export_span") or is_default_export_span + ) + self._app_root_lock = threading.Lock() + self._span_export_expectation_by_id = {} # Use the appropriate exporter based on the project key if self.public_key == project1_key: exporter = exporter_project1 @@ -1510,7 +2016,7 @@ def mock_processor_init(self, **kwargs): self, span_exporter=exporter, max_export_batch_size=512, - schedule_delay_millis=5000, + schedule_delay_millis=1, ) monkeypatch.setattr( @@ -1551,11 +2057,11 @@ def mock_initialize(self, **kwargs): # Initialize the two clients langfuse_project1 = Langfuse( - public_key=project1_key, secret_key="secret1", host="http://test-host" + public_key=project1_key, secret_key="secret1", base_url="http://test-host" ) langfuse_project2 = Langfuse( - public_key=project2_key, secret_key="secret2", host="http://test-host" + public_key=project2_key, secret_key="secret2", base_url="http://test-host" ) # Return the setup @@ -1584,12 +2090,12 @@ def mock_initialize(self, **kwargs): def test_spans_routed_to_correct_exporters(self, multi_project_setup): """Test that spans are routed to the correct exporters based on public key.""" # Create spans in both projects - span1 = multi_project_setup["langfuse_project1"].start_span( + span1 = multi_project_setup["langfuse_project1"].start_observation( name="trace-project1", metadata={"project": "project1"} ) span1.end() - span2 = multi_project_setup["langfuse_project2"].start_span( + span2 = multi_project_setup["langfuse_project2"].start_observation( name="trace-project2", metadata={"project": "project2"} ) span2.end() @@ -1622,22 +2128,22 @@ def test_concurrent_operations_in_multiple_projects(self, multi_project_setup): # Create simple non-nested spans in separate threads def create_spans_project1(): for i in range(5): - span = multi_project_setup["langfuse_project1"].start_span( + span = multi_project_setup["langfuse_project1"].start_observation( name=f"project1-span-{i}", metadata={"project": "project1", "index": i}, ) # Small sleep to ensure overlap with other thread - time.sleep(0.01) + time.sleep(0.001) span.end() def create_spans_project2(): for i in range(5): - span = multi_project_setup["langfuse_project2"].start_span( + span = multi_project_setup["langfuse_project2"].start_observation( name=f"project2-span-{i}", metadata={"project": "project2", "index": i}, ) # Small sleep to ensure overlap with other thread - time.sleep(0.01) + time.sleep(0.001) span.end() # Start threads @@ -1676,12 +2182,12 @@ def create_spans_project2(): def test_span_processor_filtering(self, multi_project_setup): """Test that spans are correctly filtered to the right exporters.""" # Create spans with identical attributes in both projects - span1 = multi_project_setup["langfuse_project1"].start_span( + span1 = multi_project_setup["langfuse_project1"].start_observation( name="test-filter-span", metadata={"project": "shared-value"} ) span1.end() - span2 = multi_project_setup["langfuse_project2"].start_span( + span2 = multi_project_setup["langfuse_project2"].start_observation( name="test-filter-span", metadata={"project": "shared-value"} ) span2.end() @@ -1721,12 +2227,12 @@ def test_context_isolation_between_projects(self, multi_project_setup): # Simplified version that just tests separate span routing # Start spans in both projects with the same name - span1 = multi_project_setup["langfuse_project1"].start_span( + span1 = multi_project_setup["langfuse_project1"].start_observation( name="identical-span-name" ) span1.end() - span2 = multi_project_setup["langfuse_project2"].start_span( + span2 = multi_project_setup["langfuse_project2"].start_observation( name="identical-span-name" ) span2.end() @@ -1752,13 +2258,13 @@ def test_cross_project_tracing(self, multi_project_setup): # Create a cross-project sequence that should not share context # Start a span in project1 - span1 = multi_project_setup["langfuse_project1"].start_span( + span1 = multi_project_setup["langfuse_project1"].start_observation( name="cross-project-parent" ) # Without ending span1, create a span in project2 # This should NOT inherit context from span1 even though it's active - span2 = multi_project_setup["langfuse_project2"].start_span( + span2 = multi_project_setup["langfuse_project2"].start_observation( name="independent-project2-span" ) @@ -1800,12 +2306,12 @@ def test_sdk_client_isolation(self, multi_project_setup): # Each client should have different trace IDs # Create two spans with identical attributes in both projects - span1 = multi_project_setup["langfuse_project1"].start_span( + span1 = multi_project_setup["langfuse_project1"].start_observation( name="isolation-test-span" ) span1.end() - span2 = multi_project_setup["langfuse_project2"].start_span( + span2 = multi_project_setup["langfuse_project2"].start_observation( name="isolation-test-span" ) span2.end() @@ -1867,13 +2373,22 @@ def instrumentation_filtering_setup(self, monkeypatch): # Mock the LangfuseSpanProcessor to use our test exporters def mock_processor_init(self, **kwargs): + import threading + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from langfuse._client.span_filter import is_default_export_span + self.public_key = kwargs.get("public_key", "test-key") blocked_scopes = kwargs.get("blocked_instrumentation_scopes") self.blocked_instrumentation_scopes = ( blocked_scopes if blocked_scopes is not None else [] ) + self._should_export_span = ( + kwargs.get("should_export_span") or is_default_export_span + ) + self._app_root_lock = threading.Lock() + self._span_export_expectation_by_id = {} # For testing, use the appropriate exporter based on setup exporter = kwargs.get("_test_exporter", blocked_exporter) @@ -1882,7 +2397,7 @@ def mock_processor_init(self, **kwargs): self, span_exporter=exporter, max_export_batch_size=512, - schedule_delay_millis=5000, + schedule_delay_millis=1, ) monkeypatch.setattr( @@ -1909,10 +2424,11 @@ def mock_initialize(self, **kwargs): processor = LangfuseSpanProcessor( public_key=self.public_key, secret_key=self.secret_key, - host=self.host, + base_url=self.base_url, blocked_instrumentation_scopes=kwargs.get( "blocked_instrumentation_scopes" ), + should_export_span=kwargs.get("should_export_span"), ) # Replace its exporter with our test exporter processor._span_exporter = blocked_exporter @@ -1946,193 +2462,301 @@ def mock_initialize(self, **kwargs): ) blocked_exporter.shutdown() - def test_blocked_instrumentation_scopes_export_filtering( + def test_default_filter_exports_langfuse_spans( self, instrumentation_filtering_setup ): - """Test that spans from blocked instrumentation scopes are not exported.""" - # Create Langfuse client with blocked scopes + """Test that the default filter exports Langfuse SDK spans.""" Langfuse( public_key=instrumentation_filtering_setup["test_key"], secret_key="test-secret-key", - host="http://localhost:3000", - blocked_instrumentation_scopes=["openai", "anthropic"], + base_url="http://localhost:3000", ) - # Get the tracer provider and create different instrumentation scope tracers tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] - - # Create langfuse tracer with proper attributes for project validation langfuse_tracer = tracer_provider.get_tracer( "langfuse-sdk", attributes={"public_key": instrumentation_filtering_setup["test_key"]}, ) - openai_tracer = tracer_provider.get_tracer("openai") - anthropic_tracer = tracer_provider.get_tracer("anthropic") - allowed_tracer = tracer_provider.get_tracer("allowed-library") - # Create spans from each tracer - langfuse_span = langfuse_tracer.start_span("langfuse-span") - langfuse_span.end() + span = langfuse_tracer.start_span("langfuse-span") + span.end() + tracer_provider.force_flush() - openai_span = openai_tracer.start_span("openai-span") - openai_span.end() + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() + ] + assert "langfuse-span" in exported_span_names - anthropic_span = anthropic_tracer.start_span("anthropic-span") - anthropic_span.end() + def test_default_filter_exports_genai_spans(self, instrumentation_filtering_setup): + """Test that the default filter exports spans with gen_ai.* attributes.""" + Langfuse( + public_key=instrumentation_filtering_setup["test_key"], + secret_key="test-secret-key", + base_url="http://localhost:3000", + ) - allowed_span = allowed_tracer.start_span("allowed-span") - allowed_span.end() + tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + unknown_tracer = tracer_provider.get_tracer("custom-framework") - # Force flush to ensure all spans are processed + span = unknown_tracer.start_span("genai-span") + span.set_attribute("gen_ai.request.model", "gpt-4o") + span.end() tracer_provider.force_flush() - # Check which spans were actually exported - exported_spans = instrumentation_filtering_setup[ - "blocked_exporter" - ].get_finished_spans() - exported_span_names = [span.name for span in exported_spans] - exported_scope_names = [ - span.instrumentation_scope.name - for span in exported_spans - if span.instrumentation_scope + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() ] + assert "genai-span" in exported_span_names - # Langfuse spans should be exported (not blocked) - assert "langfuse-span" in exported_span_names - assert "langfuse-sdk" in exported_scope_names + def test_default_filter_exports_known_instrumentor_spans( + self, instrumentation_filtering_setup + ): + """Test that the default filter exports spans from known instrumentors.""" + Langfuse( + public_key=instrumentation_filtering_setup["test_key"], + secret_key="test-secret-key", + base_url="http://localhost:3000", + ) - # Blocked scopes should NOT be exported - assert "openai-span" not in exported_span_names - assert "anthropic-span" not in exported_span_names - assert "openai" not in exported_scope_names - assert "anthropic" not in exported_scope_names + tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + known_tracer = tracer_provider.get_tracer("openinference.instrumentation.agno") - # Allowed scopes should be exported - assert "allowed-span" in exported_span_names - assert "allowed-library" in exported_scope_names + span = known_tracer.start_span("known-instrumentor-span") + span.end() + tracer_provider.force_flush() - def test_no_blocked_scopes_allows_all_exports( + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() + ] + assert "known-instrumentor-span" in exported_span_names + + def test_default_filter_rejects_unknown_spans( self, instrumentation_filtering_setup ): - """Test that when no scopes are blocked, all spans are exported.""" - # Create Langfuse client with NO blocked scopes + """Test that the default filter drops unknown scopes without gen_ai.* attrs.""" Langfuse( public_key=instrumentation_filtering_setup["test_key"], secret_key="test-secret-key", - host="http://localhost:3000", - blocked_instrumentation_scopes=[], + base_url="http://localhost:3000", ) - # Get the tracer provider and create different instrumentation scope tracers tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + unknown_tracer = tracer_provider.get_tracer("unknown.scope") - langfuse_tracer = tracer_provider.get_tracer( - "langfuse-sdk", - attributes={"public_key": instrumentation_filtering_setup["test_key"]}, - ) - openai_tracer = tracer_provider.get_tracer("openai") - anthropic_tracer = tracer_provider.get_tracer("anthropic") + span = unknown_tracer.start_span("unknown-span") + span.end() + tracer_provider.force_flush() - # Create spans from each tracer - langfuse_span = langfuse_tracer.start_span("langfuse-span") - langfuse_span.end() + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() + ] + assert "unknown-span" not in exported_span_names - openai_span = openai_tracer.start_span("openai-span") - openai_span.end() + def test_custom_should_export_span(self, instrumentation_filtering_setup): + """Test that a custom should_export_span callback controls export.""" + Langfuse( + public_key=instrumentation_filtering_setup["test_key"], + secret_key="test-secret-key", + base_url="http://localhost:3000", + should_export_span=lambda span: span.name.startswith("keep-"), + ) - anthropic_span = anthropic_tracer.start_span("anthropic-span") - anthropic_span.end() + tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + tracer = tracer_provider.get_tracer("unknown.scope") - # Force flush + keep_span = tracer.start_span("keep-span") + keep_span.end() + drop_span = tracer.start_span("drop-span") + drop_span.end() tracer_provider.force_flush() - # Check that ALL spans were exported - exported_spans = instrumentation_filtering_setup[ - "blocked_exporter" - ].get_finished_spans() - exported_span_names = [span.name for span in exported_spans] - - assert "langfuse-span" in exported_span_names - assert "openai-span" in exported_span_names - assert "anthropic-span" in exported_span_names + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() + ] + assert "keep-span" in exported_span_names + assert "drop-span" not in exported_span_names - def test_none_blocked_scopes_allows_all_exports( + def test_custom_should_export_span_with_composition( self, instrumentation_filtering_setup ): - """Test that when blocked_scopes is None (default), all spans are exported.""" - # Create Langfuse client with None blocked scopes (default behavior) + """Test composing the default filter with custom scope logic.""" + from langfuse.span_filter import is_default_export_span + Langfuse( public_key=instrumentation_filtering_setup["test_key"], secret_key="test-secret-key", - host="http://localhost:3000", - blocked_instrumentation_scopes=None, + base_url="http://localhost:3000", + should_export_span=lambda span: ( + is_default_export_span(span) + or ( + span.instrumentation_scope is not None + and span.instrumentation_scope.name.startswith("my-framework") + ) + ), ) - # Get the tracer provider and create different instrumentation scope tracers tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + custom_tracer = tracer_provider.get_tracer("my-framework.worker") + known_tracer = tracer_provider.get_tracer("ai") + unknown_tracer = tracer_provider.get_tracer("unknown.scope") + + custom_span = custom_tracer.start_span("custom-span") + custom_span.end() + known_span = known_tracer.start_span("known-span") + known_span.end() + unknown_span = unknown_tracer.start_span("unknown-span") + unknown_span.end() + tracer_provider.force_flush() - langfuse_tracer = tracer_provider.get_tracer( - "langfuse-sdk", - attributes={"public_key": instrumentation_filtering_setup["test_key"]}, - ) - openai_tracer = tracer_provider.get_tracer("openai") + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() + ] + assert "custom-span" in exported_span_names + assert "known-span" in exported_span_names + assert "unknown-span" not in exported_span_names - # Create spans from each tracer - langfuse_span = langfuse_tracer.start_span("langfuse-span") - langfuse_span.end() + def test_blocked_scopes_override_should_export( + self, instrumentation_filtering_setup + ): + """Test that blocked scopes are dropped even when callback allows all.""" + with pytest.warns(DeprecationWarning, match="blocked_instrumentation_scopes"): + Langfuse( + public_key=instrumentation_filtering_setup["test_key"], + secret_key="test-secret-key", + base_url="http://localhost:3000", + blocked_instrumentation_scopes=["my-framework.worker"], + should_export_span=lambda span: True, + ) - openai_span = openai_tracer.start_span("openai-span") - openai_span.end() + tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + blocked_tracer = tracer_provider.get_tracer("my-framework.worker") + allowed_tracer = tracer_provider.get_tracer("custom.allowed") - # Force flush + blocked_span = blocked_tracer.start_span("blocked-span") + blocked_span.end() + allowed_span = allowed_tracer.start_span("allowed-span") + allowed_span.end() tracer_provider.force_flush() - # Check that ALL spans were exported - exported_spans = instrumentation_filtering_setup[ - "blocked_exporter" - ].get_finished_spans() - exported_span_names = [span.name for span in exported_spans] + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() + ] + assert "blocked-span" not in exported_span_names + assert "allowed-span" in exported_span_names - assert "langfuse-span" in exported_span_names - assert "openai-span" in exported_span_names + def test_should_export_span_with_none_uses_default( + self, instrumentation_filtering_setup + ): + """Test that None should_export_span falls back to the default filter.""" + Langfuse( + public_key=instrumentation_filtering_setup["test_key"], + secret_key="test-secret-key", + base_url="http://localhost:3000", + should_export_span=None, + ) + + tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + known_tracer = tracer_provider.get_tracer("ai") + unknown_tracer = tracer_provider.get_tracer("unknown.scope") + + known_span = known_tracer.start_span("known-span") + known_span.end() + unknown_span = unknown_tracer.start_span("unknown-span") + unknown_span.end() + tracer_provider.force_flush() + + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() + ] + assert "known-span" in exported_span_names + assert "unknown-span" not in exported_span_names + + def test_should_export_span_exception_drops_span_and_logs_error( + self, instrumentation_filtering_setup, caplog + ): + """Test that callback failures log an error and skip exporting that span.""" + caplog.set_level("ERROR", logger="langfuse") + + def _failing_filter(_span): + raise RuntimeError("boom") - def test_blocking_langfuse_sdk_scope_export(self, instrumentation_filtering_setup): - """Test that even Langfuse's own spans are blocked if explicitly specified.""" - # Create Langfuse client that blocks its own instrumentation scope Langfuse( public_key=instrumentation_filtering_setup["test_key"], secret_key="test-secret-key", - host="http://localhost:3000", - blocked_instrumentation_scopes=["langfuse-sdk"], + base_url="http://localhost:3000", + should_export_span=_failing_filter, ) - # Get the tracer provider and create tracers tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + tracer = tracer_provider.get_tracer("unknown.scope") - langfuse_tracer = tracer_provider.get_tracer( - "langfuse-sdk", - attributes={"public_key": instrumentation_filtering_setup["test_key"]}, + span = tracer.start_span("callback-error-span") + span.end() + tracer_provider.force_flush() + + exported_span_names = [ + span.name + for span in instrumentation_filtering_setup[ + "blocked_exporter" + ].get_finished_spans() + ] + assert "callback-error-span" not in exported_span_names + assert any( + "should_export_span callback raised an error" in record.message + for record in caplog.records ) - other_tracer = tracer_provider.get_tracer("other-library") - # Create spans - langfuse_span = langfuse_tracer.start_span("langfuse-span") - langfuse_span.end() + def test_blocked_scope_drop_logs_scope_name( + self, instrumentation_filtering_setup, caplog + ): + """Test that blocked scope drops include scope names in debug logs.""" + caplog.set_level("DEBUG", logger="langfuse") - other_span = other_tracer.start_span("other-span") - other_span.end() + with pytest.warns(DeprecationWarning, match="blocked_instrumentation_scopes"): + Langfuse( + public_key=instrumentation_filtering_setup["test_key"], + secret_key="test-secret-key", + base_url="http://localhost:3000", + blocked_instrumentation_scopes=["my.blocked.scope"], + should_export_span=lambda span: True, + ) - # Force flush - tracer_provider.force_flush() + tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] + blocked_tracer = tracer_provider.get_tracer("my.blocked.scope") - # Check exports - Langfuse spans should be blocked, others allowed - exported_spans = instrumentation_filtering_setup[ - "blocked_exporter" - ].get_finished_spans() - exported_span_names = [span.name for span in exported_spans] + span = blocked_tracer.start_span("blocked-debug-span") + span.end() + tracer_provider.force_flush() - assert "langfuse-span" not in exported_span_names - assert "other-span" in exported_span_names + assert any( + "Dropping span due to blocked instrumentation scope" in record.message + and "my.blocked.scope" in record.message + for record in caplog.records + ) class TestConcurrencyAndAsync(TestOTelBase): @@ -2144,15 +2768,15 @@ async def test_async_span_operations(self, langfuse_client, memory_exporter): import asyncio # Start a main span - main_span = langfuse_client.start_span(name="async-main-span") + main_span = langfuse_client.start_observation(name="async-main-span") # Define an async function that creates and updates spans async def async_task(parent_span, task_id): # Start a child span - child_span = parent_span.start_span(name=f"async-task-{task_id}") + child_span = parent_span.start_observation(name=f"async-task-{task_id}") # Simulate async work - await asyncio.sleep(0.1) + await asyncio.sleep(0.01) # Update span with results child_span.update( @@ -2217,7 +2841,7 @@ def test_context_propagation_async(self, langfuse_client, memory_exporter): # Create a main span in thread 1 trace_context = {"trace_id": trace_id} - main_span = langfuse_client.start_span( + main_span = langfuse_client.start_observation( name="main-async-span", trace_context=trace_context ) @@ -2238,7 +2862,7 @@ def thread2_function(): nonlocal thread2_span_id, thread2_trace_id # Access the same trace via trace_id in a different thread - thread2_span = langfuse_client.start_span( + thread2_span = langfuse_client.start_observation( name="thread2-span", trace_context={"trace_id": trace_id} ) @@ -2257,7 +2881,7 @@ def thread3_function(): nonlocal thread3_span_id, thread3_trace_id # Create a child of the main span by providing parent_span_id - thread3_span = langfuse_client.start_span( + thread3_span = langfuse_client.start_observation( name="thread3-span", trace_context={"trace_id": trace_id, "parent_span_id": main_span_id}, ) @@ -2305,14 +2929,14 @@ def thread3_function(): assert thread3_span["trace_id"] == trace_id # Verify thread2 span is at the root level (no parent within our trace) - assert ( - thread2_span["attributes"][LangfuseOtelSpanAttributes.AS_ROOT] is True - ), "Thread 2 span should not have a parent" + assert thread2_span["attributes"][LangfuseOtelSpanAttributes.AS_ROOT] is True, ( + "Thread 2 span should not have a parent" + ) # Verify thread3 span is a child of the main span - assert ( - thread3_span["parent_span_id"] == main_span_id - ), "Thread 3 span should be a child of main span" + assert thread3_span["parent_span_id"] == main_span_id, ( + "Thread 3 span should be a child of main span" + ) @pytest.mark.asyncio async def test_span_metadata_updates_in_async_context( @@ -2320,13 +2944,13 @@ async def test_span_metadata_updates_in_async_context( ): """Test that span metadata updates preserve nested values in async contexts.""" # Skip if the client setup is causing recursion issues - if not hasattr(langfuse_client, "start_span"): + if not hasattr(langfuse_client, "start_observation"): pytest.skip("Client setup has issues, skipping test") import asyncio # Create a trace with a main span - with langfuse_client.start_as_current_span( + with langfuse_client.start_as_current_observation( name="async-metadata-test" ) as main_span: # Initial metadata with nested structure @@ -2343,7 +2967,7 @@ async def test_span_metadata_updates_in_async_context( # Define async tasks that update different parts of metadata async def update_temperature(): - await asyncio.sleep(0.1) # Simulate some async work + await asyncio.sleep(0.01) # Simulate some async work main_span.update( metadata={ "llm_config": { @@ -2355,7 +2979,7 @@ async def update_temperature(): ) async def update_model(): - await asyncio.sleep(0.05) # Simulate some async work + await asyncio.sleep(0.005) # Simulate some async work main_span.update( metadata={ "llm_config": { @@ -2365,7 +2989,7 @@ async def update_model(): ) async def add_context_length(): - await asyncio.sleep(0.15) # Simulate some async work + await asyncio.sleep(0.015) # Simulate some async work main_span.update( metadata={ "llm_config": { @@ -2377,7 +3001,7 @@ async def add_context_length(): ) async def update_user_id(): - await asyncio.sleep(0.08) # Simulate some async work + await asyncio.sleep(0.008) # Simulate some async work main_span.update( metadata={ "request_info": { @@ -2439,7 +3063,7 @@ def test_metrics_and_timing(self, langfuse_client, memory_exporter): start_time = time.time() # Create a span - span = langfuse_client.start_span(name="timing-test-span") + span = langfuse_client.start_observation(name="timing-test-span") # Add a small delay time.sleep(0.1) @@ -2471,12 +3095,12 @@ def test_metrics_and_timing(self, langfuse_client, memory_exporter): # The span timing should be within our manually recorded range # Note: This might fail on slow systems, so we use a relaxed comparison - assert ( - span_start_seconds <= end_time - ), "Span start time should be before our recorded end time" - assert ( - span_end_seconds >= start_time - ), "Span end time should be after our recorded start time" + assert span_start_seconds <= end_time, ( + "Span start time should be before our recorded end time" + ) + assert span_end_seconds >= start_time, ( + "Span end time should be after our recorded start time" + ) # Span duration should be positive and roughly match our sleep time span_duration_seconds = ( @@ -2486,9 +3110,9 @@ def test_metrics_and_timing(self, langfuse_client, memory_exporter): # Since we slept for 0.1 seconds, the span duration should be at least 0.05 seconds # but we'll be generous with the upper bound due to potential system delays - assert ( - span_duration_seconds >= 0.05 - ), f"Span duration ({span_duration_seconds}s) should be at least 0.05s" + assert span_duration_seconds >= 0.05, ( + f"Span duration ({span_duration_seconds}s) should be at least 0.05s" + ) # Add tests for media functionality in its own class @@ -2682,9 +3306,9 @@ def mask_sensitive_data(data): # Run all test cases for i, test_case in enumerate(test_cases): result = mask_sensitive_data(test_case["input"]) - assert ( - result == test_case["expected"] - ), f"Test case {i} failed: {result} != {test_case['expected']}" + assert result == test_case["expected"], ( + f"Test case {i} failed: {result} != {test_case['expected']}" + ) # Now test using the actual LangfuseSpan implementation from unittest.mock import MagicMock @@ -2743,7 +3367,8 @@ def langfuse_client(self, monkeypatch): client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", + tracing_enabled=False, ) return client @@ -2851,3 +3476,33 @@ def test_different_seeds_produce_different_ids(self, langfuse_client): # All observation IDs should be unique assert len(set(observation_ids)) == len(seeds) + + def test_langfuse_event_update_immutability(self, langfuse_client, caplog): + """Test that LangfuseEvent.update() logs a warning and does nothing.""" + import logging + + parent_span = langfuse_client.start_observation(name="parent-span") + + event = parent_span.start_observation( + name="test-event", + as_type="event", + input={"original": "input"}, + ) + + # Try to update the event and capture warning logs + with caplog.at_level(logging.WARNING, logger="langfuse._client.span"): + result = event.update( + name="updated_name", + input={"updated": "input"}, + output={"updated": "output"}, + metadata={"updated": "metadata"}, + ) + + # Verify warning was logged + assert "Attempted to update LangfuseEvent observation" in caplog.text + assert "Events cannot be updated after creation" in caplog.text + + # Verify the method returned self unchanged + assert result is event + + parent_span.end() diff --git a/tests/unit/test_parse_usage_model.py b/tests/unit/test_parse_usage_model.py new file mode 100644 index 000000000..df441523c --- /dev/null +++ b/tests/unit/test_parse_usage_model.py @@ -0,0 +1,34 @@ +from langfuse.langchain.CallbackHandler import _parse_usage_model + + +def test_standard_tier_input_token_details(): + """Standard tier: audio and cache_read are subtracted from input.""" + usage = { + "input_tokens": 13, + "output_tokens": 1, + "total_tokens": 14, + "input_token_details": {"audio": 0, "cache_read": 3}, + "output_token_details": {"audio": 0}, + } + result = _parse_usage_model(usage) + assert result["input"] == 10 # 13 - 0 (audio) - 3 (cache_read) + assert result["output"] == 1 # 1 - 0 (audio) + assert result["total"] == 14 + + +def test_priority_tier_not_subtracted(): + """Priority tier: 'priority' and 'priority_*' keys must NOT be subtracted.""" + usage = { + "input_tokens": 13, + "output_tokens": 1, + "total_tokens": 14, + "input_token_details": {"audio": 0, "priority_cache_read": 0, "priority": 13}, + "output_token_details": {"audio": 0, "priority_reasoning": 0, "priority": 1}, + } + result = _parse_usage_model(usage) + assert result["input"] == 13 # priority keys not subtracted + assert result["output"] == 1 + assert result["total"] == 14 + # Priority keys are still stored with prefixed names + assert result["input_priority"] == 13 + assert result["output_priority"] == 1 diff --git a/tests/unit/test_prompt.py b/tests/unit/test_prompt.py new file mode 100644 index 000000000..eadfb8221 --- /dev/null +++ b/tests/unit/test_prompt.py @@ -0,0 +1,750 @@ +from unittest.mock import Mock, patch + +import pytest + +from langfuse._client.client import Langfuse +from langfuse._utils.prompt_cache import ( + DEFAULT_PROMPT_CACHE_TTL_SECONDS, + PromptCache, + PromptCacheItem, + PromptCacheTaskManager, +) +from langfuse.api import NotFoundError, Prompt_Chat, Prompt_Text +from langfuse.model import ChatPromptClient, TextPromptClient + + +@pytest.mark.parametrize( + ("variables", "placeholders", "expected_len", "expected_contents"), + [ + ( + {"role": "helpful", "task": "coding"}, + {}, + 3, + ["You are a helpful assistant", None, "Help me with coding"], + ), + ( + {}, + {}, + 3, + ["You are a {{role}} assistant", None, "Help me with {{task}}"], + ), + ( + {}, + { + "examples": [ + {"role": "user", "content": "Example question"}, + {"role": "assistant", "content": "Example answer"}, + ], + }, + 4, + [ + "You are a {{role}} assistant", + "Example question", + "Example answer", + "Help me with {{task}}", + ], + ), + ( + {"role": "helpful", "task": "coding"}, + { + "examples": [ + {"role": "user", "content": "Show me {{task}}"}, + {"role": "assistant", "content": "Here's {{task}}"}, + ], + }, + 4, + [ + "You are a helpful assistant", + "Show me coding", + "Here's coding", + "Help me with coding", + ], + ), + ( + {"role": "helpful", "task": "coding"}, + {"unused": [{"role": "user", "content": "Won't appear"}]}, + 3, + ["You are a helpful assistant", None, "Help me with coding"], + ), + ( + {"role": "helpful", "task": "coding"}, + {"examples": "not a list"}, + 3, + [ + "You are a helpful assistant", + "not a list", + "Help me with coding", + ], + ), + ( + {"role": "helpful", "task": "coding"}, + { + "examples": [ + "invalid message", + {"role": "user", "content": "valid message"}, + ] + }, + 4, + [ + "You are a helpful assistant", + "['invalid message', {'role': 'user', 'content': 'valid message'}]", + "valid message", + "Help me with coding", + ], + ), + ], +) +def test_compile_with_placeholders( + variables, placeholders, expected_len, expected_contents +) -> None: + mock_prompt = Prompt_Chat( + name="test_prompt", + version=1, + type="chat", + config={}, + tags=[], + labels=[], + prompt=[ + {"role": "system", "content": "You are a {{role}} assistant"}, + {"type": "placeholder", "name": "examples"}, + {"role": "user", "content": "Help me with {{task}}"}, + ], + ) + + compile_kwargs = {**placeholders, **variables} + result = ChatPromptClient(mock_prompt).compile(**compile_kwargs) + + assert len(result) == expected_len + for i, expected_content in enumerate(expected_contents): + if expected_content is None: + assert "type" in result[i] and result[i]["type"] == "placeholder" + elif isinstance(result[i], str): + assert result[i] == expected_content + else: + assert "content" in result[i] + assert result[i]["content"] == expected_content + + +@pytest.fixture +def langfuse(): + from langfuse._client.resource_manager import LangfuseResourceManager + + langfuse_instance = Langfuse() + langfuse_instance.api = Mock() + + if langfuse_instance._resources is None: + langfuse_instance._resources = Mock(spec=LangfuseResourceManager) + langfuse_instance._resources.prompt_cache = PromptCache() + + return langfuse_instance + + +def wait_for_prompt_refresh(langfuse: Langfuse) -> None: + langfuse._resources.prompt_cache._task_manager.wait_for_idle() + + +def test_prompt_cache_task_manager_pauses_all_workers_before_broadcasting_shutdown(): + manager = PromptCacheTaskManager(threads=0) + events = [] + + class FakeConsumer: + def __init__(self, identifier): + self._identifier = identifier + + def pause(self): + events.append(("pause", self._identifier)) + + def join(self): + events.append(("join", self._identifier)) + + class FakeQueue: + def put(self, item): + events.append(("put", item)) + + manager._consumers = [FakeConsumer(0), FakeConsumer(1), FakeConsumer(2)] + manager._queue = FakeQueue() + + manager.shutdown() + + assert [event[0] for event in events] == [ + "pause", + "pause", + "pause", + "put", + "put", + "put", + "join", + "join", + "join", + ] + + +def test_get_fresh_prompt(langfuse): + prompt_name = "test_get_fresh_prompt" + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + type="text", + labels=[], + config={}, + tags=[], + ) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result = langfuse.get_prompt(prompt_name, fallback="fallback") + mock_server_call.assert_called_once_with( + prompt_name, + version=None, + label=None, + request_options=None, + ) + + assert result == TextPromptClient(prompt) + + +def test_throw_if_name_unspecified(langfuse): + with pytest.raises(ValueError) as exc_info: + langfuse.get_prompt("") + + assert "Prompt name cannot be empty" in str(exc_info.value) + + +def test_throw_when_failing_fetch_and_no_cache(langfuse): + mock_server_call = langfuse.api.prompts.get + mock_server_call.side_effect = Exception("Prompt not found") + + with pytest.raises(Exception) as exc_info: + langfuse.get_prompt("failing_fetch_and_no_cache") + + assert "Prompt not found" in str(exc_info.value) + + +def test_using_custom_prompt_timeouts(langfuse): + prompt_name = "test_using_custom_prompt_timeouts" + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + type="text", + labels=[], + config={}, + tags=[], + ) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result = langfuse.get_prompt( + prompt_name, fallback="fallback", fetch_timeout_seconds=1000 + ) + mock_server_call.assert_called_once_with( + prompt_name, + version=None, + label=None, + request_options={"timeout_in_seconds": 1000}, + ) + + assert result == TextPromptClient(prompt) + + +def test_throw_if_cache_ttl_seconds_positional_argument(langfuse): + with pytest.raises(TypeError) as exc_info: + langfuse.get_prompt("test ttl seconds in positional arg", 20) + + assert "positional arguments" in str(exc_info.value) + + +def test_get_valid_cached_prompt(langfuse): + prompt_name = "test_get_valid_cached_prompt" + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + type="text", + labels=[], + config={}, + tags=[], + ) + prompt_client = TextPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name, fallback="fallback") + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + result_call_2 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_2 == prompt_client + + +def test_get_valid_cached_chat_prompt_by_label(langfuse): + prompt_name = "test_get_valid_cached_chat_prompt_by_label" + prompt = Prompt_Chat( + name=prompt_name, + version=1, + prompt=[{"role": "system", "content": "Make me laugh"}], + labels=["test"], + type="chat", + config={}, + tags=[], + ) + prompt_client = ChatPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name, label="test") + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + result_call_2 = langfuse.get_prompt(prompt_name, label="test") + assert mock_server_call.call_count == 1 + assert result_call_2 == prompt_client + + +def test_get_valid_cached_chat_prompt_by_version(langfuse): + prompt_name = "test_get_valid_cached_chat_prompt_by_version" + prompt = Prompt_Chat( + name=prompt_name, + version=1, + prompt=[{"role": "system", "content": "Make me laugh"}], + labels=["test"], + type="chat", + config={}, + tags=[], + ) + prompt_client = ChatPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name, version=1) + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + result_call_2 = langfuse.get_prompt(prompt_name, version=1) + assert mock_server_call.call_count == 1 + assert result_call_2 == prompt_client + + +def test_get_valid_cached_production_chat_prompt(langfuse): + prompt_name = "test_get_valid_cached_production_chat_prompt" + prompt = Prompt_Chat( + name=prompt_name, + version=1, + prompt=[{"role": "system", "content": "Make me laugh"}], + labels=["test"], + type="chat", + config={}, + tags=[], + ) + prompt_client = ChatPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + result_call_2 = langfuse.get_prompt(prompt_name, label="production") + assert mock_server_call.call_count == 1 + assert result_call_2 == prompt_client + + +def test_get_valid_cached_chat_prompt(langfuse): + prompt_name = "test_get_valid_cached_chat_prompt" + prompt = Prompt_Chat( + name=prompt_name, + version=1, + prompt=[{"role": "system", "content": "Make me laugh"}], + labels=[], + type="chat", + config={}, + tags=[], + ) + prompt_client = ChatPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + result_call_2 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_2 == prompt_client + + +@patch.object(PromptCacheItem, "get_epoch_seconds") +def test_get_fresh_prompt_when_expired_cache_custom_ttl(mock_time, langfuse: Langfuse): + mock_time.return_value = 0 + ttl_seconds = 20 + + prompt_name = "test_get_fresh_prompt_when_expired_cache_custom_ttl" + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + config={"temperature": 0.9}, + labels=[], + type="text", + tags=[], + ) + prompt_client = TextPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name, cache_ttl_seconds=ttl_seconds) + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + mock_time.return_value = ttl_seconds - 1 + + result_call_2 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_2 == prompt_client + + mock_time.return_value = ttl_seconds + 1 + + result_call_3 = langfuse.get_prompt(prompt_name) + + wait_for_prompt_refresh(langfuse) + + assert mock_server_call.call_count == 2 + assert result_call_3 == prompt_client + + +@patch.object(PromptCacheItem, "get_epoch_seconds") +def test_disable_caching_when_ttl_zero(mock_time, langfuse: Langfuse): + mock_time.return_value = 0 + prompt_name = "test_disable_caching_when_ttl_zero" + + prompt1 = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + prompt2 = Prompt_Text( + name=prompt_name, + version=2, + prompt="Tell me a joke", + labels=[], + type="text", + config={}, + tags=[], + ) + prompt3 = Prompt_Text( + name=prompt_name, + version=3, + prompt="Share a funny story", + labels=[], + type="text", + config={}, + tags=[], + ) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.side_effect = [prompt1, prompt2, prompt3] + + result1 = langfuse.get_prompt(prompt_name, cache_ttl_seconds=0) + assert mock_server_call.call_count == 1 + assert result1 == TextPromptClient(prompt1) + + result2 = langfuse.get_prompt(prompt_name, cache_ttl_seconds=0) + assert mock_server_call.call_count == 2 + assert result2 == TextPromptClient(prompt2) + + result3 = langfuse.get_prompt(prompt_name, cache_ttl_seconds=0) + assert mock_server_call.call_count == 3 + assert result3 == TextPromptClient(prompt3) + + assert result1 != result2 != result3 + + +@patch.object(PromptCacheItem, "get_epoch_seconds") +def test_get_stale_prompt_when_expired_cache_default_ttl(mock_time, langfuse: Langfuse): + import logging + + logging.basicConfig(level=logging.DEBUG) + mock_time.return_value = 0 + + prompt_name = "test_get_stale_prompt_when_expired_cache_default_ttl" + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + prompt_client = TextPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + updated_prompt = Prompt_Text( + name=prompt_name, + version=2, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + mock_server_call.return_value = updated_prompt + + mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1 + + stale_result = langfuse.get_prompt(prompt_name) + assert stale_result == prompt_client + + langfuse.get_prompt(prompt_name) + langfuse.get_prompt(prompt_name) + langfuse.get_prompt(prompt_name) + langfuse.get_prompt(prompt_name) + + wait_for_prompt_refresh(langfuse) + + assert mock_server_call.call_count == 2 + + updated_result = langfuse.get_prompt(prompt_name) + assert updated_result.version == 2 + assert updated_result == TextPromptClient(updated_prompt) + + +@patch.object(PromptCacheItem, "get_epoch_seconds") +def test_skip_redundant_refresh_when_cache_already_updated( + mock_time, langfuse: Langfuse +) -> None: + prompt_name = "test_skip_redundant_refresh_when_cache_already_updated" + cache_key = PromptCache.generate_cache_key(prompt_name, version=None, label=None) + + mock_time.return_value = 0 + + initial_prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + updated_prompt = Prompt_Text( + name=prompt_name, + version=2, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + + stale_result = TextPromptClient(initial_prompt) + fresh_result = TextPromptClient(updated_prompt) + + langfuse._resources.prompt_cache.set(cache_key, stale_result, None) + stale_item = langfuse._resources.prompt_cache.get(cache_key) + assert stale_item is not None + + mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1 + assert stale_item.is_expired() + + langfuse._resources.prompt_cache.set(cache_key, fresh_result, None) + + add_task_mock = Mock() + langfuse._resources.prompt_cache._task_manager.add_task = add_task_mock + + langfuse._resources.prompt_cache.add_refresh_prompt_task_if_current( + cache_key, + stale_item, + Mock(), + ) + + add_task_mock.assert_not_called() + + +@patch.object(PromptCacheItem, "get_epoch_seconds") +def test_get_fresh_prompt_when_expired_cache_default_ttl(mock_time, langfuse: Langfuse): + mock_time.return_value = 0 + + prompt_name = "test_get_fresh_prompt_when_expired_cache_default_ttl" + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + prompt_client = TextPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS - 1 + + result_call_2 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_2 == prompt_client + + mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1 + + result_call_3 = langfuse.get_prompt(prompt_name) + wait_for_prompt_refresh(langfuse) + + assert mock_server_call.call_count == 2 + assert result_call_3 == prompt_client + + +@patch.object(PromptCacheItem, "get_epoch_seconds") +def test_get_expired_prompt_when_failing_fetch(mock_time, langfuse: Langfuse): + mock_time.return_value = 0 + + prompt_name = "test_get_expired_prompt_when_failing_fetch" + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + prompt_client = TextPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name) + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + mock_time.return_value = DEFAULT_PROMPT_CACHE_TTL_SECONDS + 1 + mock_server_call.side_effect = Exception("Server error") + + result_call_2 = langfuse.get_prompt(prompt_name, max_retries=1) + wait_for_prompt_refresh(langfuse) + + assert mock_server_call.call_count == 3 + assert result_call_2 == prompt_client + + +@patch.object(PromptCacheItem, "get_epoch_seconds") +def test_evict_prompt_cache_entry_when_refresh_returns_not_found( + mock_time, langfuse: Langfuse +) -> None: + mock_time.return_value = 0 + + prompt_name = "test_evict_prompt_cache_entry_when_refresh_returns_not_found" + ttl_seconds = 5 + fallback_prompt = "fallback text prompt" + + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + prompt_client = TextPromptClient(prompt) + cache_key = PromptCache.generate_cache_key(prompt_name, version=None, label=None) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + initial_result = langfuse.get_prompt( + prompt_name, + cache_ttl_seconds=ttl_seconds, + max_retries=0, + ) + assert initial_result == prompt_client + assert langfuse._resources.prompt_cache.get(cache_key) is not None + + mock_time.return_value = ttl_seconds + 1 + + def raise_not_found(*_args: object, **_kwargs: object) -> None: + raise NotFoundError({"message": "Prompt not found"}) + + mock_server_call.side_effect = raise_not_found + + stale_result = langfuse.get_prompt( + prompt_name, + cache_ttl_seconds=ttl_seconds, + max_retries=0, + ) + assert stale_result == prompt_client + + wait_for_prompt_refresh(langfuse) + + assert langfuse._resources.prompt_cache.get(cache_key) is None + + fallback_result = langfuse.get_prompt( + prompt_name, + cache_ttl_seconds=ttl_seconds, + fallback=fallback_prompt, + max_retries=0, + ) + assert fallback_result.is_fallback + assert fallback_result.prompt == fallback_prompt + + +def test_get_fresh_prompt_when_version_changes(langfuse: Langfuse): + prompt_name = "test_get_fresh_prompt_when_version_changes" + prompt = Prompt_Text( + name=prompt_name, + version=1, + prompt="Make me laugh", + labels=[], + type="text", + config={}, + tags=[], + ) + prompt_client = TextPromptClient(prompt) + + mock_server_call = langfuse.api.prompts.get + mock_server_call.return_value = prompt + + result_call_1 = langfuse.get_prompt(prompt_name, version=1) + assert mock_server_call.call_count == 1 + assert result_call_1 == prompt_client + + version_changed_prompt = Prompt_Text( + name=prompt_name, + version=2, + labels=[], + prompt="Make me laugh", + type="text", + config={}, + tags=[], + ) + version_changed_prompt_client = TextPromptClient(version_changed_prompt) + mock_server_call.return_value = version_changed_prompt + + result_call_2 = langfuse.get_prompt(prompt_name, version=2) + assert mock_server_call.call_count == 2 + assert result_call_2 == version_changed_prompt_client diff --git a/tests/test_prompt_atexit.py b/tests/unit/test_prompt_atexit.py similarity index 86% rename from tests/test_prompt_atexit.py rename to tests/unit/test_prompt_atexit.py index 9f8838adb..ccd1b0f19 100644 --- a/tests/test_prompt_atexit.py +++ b/tests/unit/test_prompt_atexit.py @@ -26,7 +26,9 @@ def wait_2_sec(): # 8 times for i in range(8): - prompt_cache.add_refresh_prompt_task(f"key_wait_2_sec_i_{i}", lambda: wait_2_sec()) + prompt_cache.add_refresh_prompt_task( + f"key_wait_2_sec_i_{i}", lambda: wait_2_sec() + ) """ process = subprocess.Popen( @@ -50,9 +52,9 @@ def wait_2_sec(): print(process.stderr) shutdown_count = logs.count("Shutdown of prompt refresh task manager completed.") - assert ( - shutdown_count == 1 - ), f"Expected 1 shutdown messages, but found {shutdown_count}" + assert shutdown_count == 1, ( + f"Expected 1 shutdown messages, but found {shutdown_count}" + ) @pytest.mark.timeout(10) @@ -79,7 +81,9 @@ def wait_2_sec(): time.sleep(2) async def add_new_prompt_refresh(i: int): - prompt_cache.add_refresh_prompt_task(f"key_wait_2_sec_i_{i}", lambda: wait_2_sec()) + prompt_cache.add_refresh_prompt_task( + f"key_wait_2_sec_i_{i}", lambda: wait_2_sec() + ) # 8 times tasks = [add_new_prompt_refresh(i) for i in range(8)] @@ -114,6 +118,6 @@ async def run_multiple_mains(): print(process.stderr) shutdown_count = logs.count("Shutdown of prompt refresh task manager completed.") - assert ( - shutdown_count == 3 - ), f"Expected 3 shutdown messages, but found {shutdown_count}" + assert shutdown_count == 3, ( + f"Expected 3 shutdown messages, but found {shutdown_count}" + ) diff --git a/tests/test_prompt_compilation.py b/tests/unit/test_prompt_compilation.py similarity index 88% rename from tests/test_prompt_compilation.py rename to tests/unit/test_prompt_compilation.py index c8aa789dc..1b96a14dd 100644 --- a/tests/test_prompt_compilation.py +++ b/tests/unit/test_prompt_compilation.py @@ -1,7 +1,7 @@ import pytest -from langchain.prompts import ChatPromptTemplate, PromptTemplate +from langchain_core.prompts import ChatPromptTemplate, PromptTemplate -from langfuse.api.resources.prompts import ChatMessage, Prompt_Chat +from langfuse.api import ChatMessage, Prompt_Chat from langfuse.model import ( ChatPromptClient, Prompt_Text, @@ -735,7 +735,7 @@ def test_chat_prompt_with_json_variables(self): def test_chat_prompt_with_placeholders_langchain(self): """Test that chat prompts with placeholders work correctly with Langchain.""" - from langfuse.api.resources.prompts import Prompt_Chat + from langfuse.api import Prompt_Chat chat_messages = [ ChatMessage( @@ -804,7 +804,7 @@ def test_chat_prompt_with_placeholders_langchain(self): def test_get_langchain_prompt_with_unresolved_placeholders(self): """Test that unresolved placeholders become MessagesPlaceholder objects.""" - from langfuse.api.resources.prompts import Prompt_Chat + from langfuse.api import Prompt_Chat from langfuse.model import ChatPromptClient chat_messages = [ @@ -850,3 +850,85 @@ def test_get_langchain_prompt_with_unresolved_placeholders(self): # Third message should be the user message assert langchain_messages[2] == ("user", "Help me with coding") + + +def test_tool_calls_preservation_in_message_placeholder(): + """Test that tool calls are preserved when compiling message placeholders.""" + from langfuse.api import Prompt_Chat + + chat_messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"type": "placeholder", "name": "message_history"}, + {"role": "user", "content": "Help me with {{task}}"}, + ] + + prompt_client = ChatPromptClient( + Prompt_Chat( + type="chat", + name="tool_calls_test", + version=1, + config={}, + tags=[], + labels=[], + prompt=chat_messages, + ) + ) + + # Message history with tool calls - exactly like the bug report describes + message_history_with_tool_calls = [ + {"role": "user", "content": "What's the weather like?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"location": "San Francisco"}', + }, + } + ], + }, + { + "role": "tool", + "content": "It's sunny, 72°F", + "tool_call_id": "call_123", + "name": "get_weather", + }, + ] + + # Compile with message history and variables + compiled_messages = prompt_client.compile( + task="weather inquiry", message_history=message_history_with_tool_calls + ) + + # Should have 5 messages: system + 3 from history + user + assert len(compiled_messages) == 5 + + # System message + assert compiled_messages[0]["role"] == "system" + assert compiled_messages[0]["content"] == "You are a helpful assistant." + + # User message from history + assert compiled_messages[1]["role"] == "user" + assert compiled_messages[1]["content"] == "What's the weather like?" + + # Assistant message with TOOL CALLS + assert compiled_messages[2]["role"] == "assistant" + assert compiled_messages[2]["content"] == "" + assert "tool_calls" in compiled_messages[2] + assert len(compiled_messages[2]["tool_calls"]) == 1 + assert compiled_messages[2]["tool_calls"][0]["id"] == "call_123" + assert compiled_messages[2]["tool_calls"][0]["function"]["name"] == "get_weather" + + # TOOL CALL results message + assert compiled_messages[3]["role"] == "tool" + assert compiled_messages[3]["content"] == "It's sunny, 72°F" + assert compiled_messages[3]["tool_call_id"] == "call_123" + assert compiled_messages[3]["name"] == "get_weather" + + # Final user message with compiled variable + assert compiled_messages[4]["role"] == "user" + assert compiled_messages[4]["content"] == "Help me with weather inquiry" diff --git a/tests/unit/test_propagate_attributes.py b/tests/unit/test_propagate_attributes.py new file mode 100644 index 000000000..c783e65dd --- /dev/null +++ b/tests/unit/test_propagate_attributes.py @@ -0,0 +1,3139 @@ +"""Comprehensive tests for propagate_attributes functionality. + +This module tests the propagate_attributes context manager that allows setting +trace-level attributes (user_id, session_id, metadata) that automatically propagate +to all child spans within the context. +""" + +import concurrent.futures +from datetime import datetime + +import pytest +from opentelemetry.instrumentation.threading import ThreadingInstrumentor + +from langfuse import propagate_attributes +from langfuse._client.attributes import LangfuseOtelSpanAttributes, _serialize +from langfuse._client.constants import LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT +from langfuse._client.datasets import DatasetClient +from langfuse.api import Dataset, DatasetItem, DatasetStatus +from tests.unit.test_otel import TestOTelBase + + +class TestPropagateAttributesBase(TestOTelBase): + """Base class for propagate_attributes tests with shared helper methods.""" + + @pytest.fixture + def langfuse_client(self, monkeypatch, tracer_provider, mock_processor_init): + """Create a mocked Langfuse client with explicit tracer_provider for testing.""" + from langfuse import Langfuse + + # Set environment variables + monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "test-public-key") + monkeypatch.setenv("LANGFUSE_SECRET_KEY", "test-secret-key") + + # Create test client with explicit tracer_provider + client = Langfuse( + public_key="test-public-key", + secret_key="test-secret-key", + host="http://test-host", + tracing_enabled=True, + tracer_provider=tracer_provider, # Pass the test provider explicitly + ) + + yield client + + def get_span_by_name(self, memory_exporter, name: str) -> dict: + """Get single span by name (assert exactly one exists). + + Args: + memory_exporter: The in-memory span exporter fixture + name: The name of the span to retrieve + + Returns: + dict: The span data as a dictionary + + Raises: + AssertionError: If zero or more than one span with the name exists + """ + spans = self.get_spans_by_name(memory_exporter, name) + assert len(spans) > 0, f"Expected at least 1 span named '{name}'" + return spans[0] + + def verify_missing_attribute(self, span_data: dict, attr_key: str): + """Verify that a span does NOT have a specific attribute. + + Args: + span_data: The span data dictionary + attr_key: The attribute key to check for absence + + Raises: + AssertionError: If the attribute exists on the span + """ + attributes = span_data["attributes"] + assert attr_key not in attributes, ( + f"Attribute '{attr_key}' should NOT be on span '{span_data['name']}'" + ) + + +class TestPropagateAttributesBasic(TestPropagateAttributesBase): + """Tests for basic propagate_attributes functionality.""" + + def test_user_id_propagates_to_child_spans(self, langfuse_client, memory_exporter): + """Verify user_id propagates to all child spans within context.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id="test_user_123"): + child1 = langfuse_client.start_observation(name="child-span-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-span-2") + child2.end() + + # Verify both children have user_id + child1_span = self.get_span_by_name(memory_exporter, "child-span-1") + self.verify_span_attribute( + child1_span, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "test_user_123", + ) + + child2_span = self.get_span_by_name(memory_exporter, "child-span-2") + self.verify_span_attribute( + child2_span, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "test_user_123", + ) + + def test_session_id_propagates_to_child_spans( + self, langfuse_client, memory_exporter + ): + """Verify session_id propagates to all child spans within context.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(session_id="session_abc"): + child1 = langfuse_client.start_observation(name="child-span-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-span-2") + child2.end() + + # Verify both children have session_id + child1_span = self.get_span_by_name(memory_exporter, "child-span-1") + self.verify_span_attribute( + child1_span, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "session_abc", + ) + + child2_span = self.get_span_by_name(memory_exporter, "child-span-2") + self.verify_span_attribute( + child2_span, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "session_abc", + ) + + def test_metadata_propagates_to_child_spans(self, langfuse_client, memory_exporter): + """Verify metadata propagates to all child spans within context.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={"experiment": "variant_a", "version": "1.0"} + ): + child1 = langfuse_client.start_observation(name="child-span-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-span-2") + child2.end() + + # Verify both children have metadata + child1_span = self.get_span_by_name(memory_exporter, "child-span-1") + self.verify_span_attribute( + child1_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", + "variant_a", + ) + self.verify_span_attribute( + child1_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", + "1.0", + ) + + child2_span = self.get_span_by_name(memory_exporter, "child-span-2") + self.verify_span_attribute( + child2_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", + "variant_a", + ) + self.verify_span_attribute( + child2_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", + "1.0", + ) + + def test_all_attributes_propagate_together(self, langfuse_client, memory_exporter): + """Verify user_id, session_id, and metadata all propagate together.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + user_id="user_123", + session_id="session_abc", + metadata={"experiment": "test", "env": "prod"}, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child has all attributes + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session_abc" + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", + "test", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "prod", + ) + + +class TestPropagateAttributesHierarchy(TestPropagateAttributesBase): + """Tests for propagation across span hierarchies.""" + + def test_propagation_to_direct_children(self, langfuse_client, memory_exporter): + """Verify attributes propagate to all direct children.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id="user_123"): + child1 = langfuse_client.start_observation(name="child-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-2") + child2.end() + + child3 = langfuse_client.start_observation(name="child-3") + child3.end() + + # Verify all three children have user_id + for i in range(1, 4): + child_span = self.get_span_by_name(memory_exporter, f"child-{i}") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + def test_propagation_to_grandchildren(self, langfuse_client, memory_exporter): + """Verify attributes propagate through multiple levels of nesting.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id="user_123", session_id="session_abc"): + with langfuse_client.start_as_current_observation(name="child-span"): + grandchild = langfuse_client.start_observation( + name="grandchild-span" + ) + grandchild.end() + + # Verify all three levels have attributes + parent_span = self.get_span_by_name(memory_exporter, "parent-span") + child_span = self.get_span_by_name(memory_exporter, "child-span") + grandchild_span = self.get_span_by_name(memory_exporter, "grandchild-span") + + for span in [parent_span, child_span, grandchild_span]: + self.verify_span_attribute( + span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + self.verify_span_attribute( + span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session_abc" + ) + + def test_propagation_across_observation_types( + self, langfuse_client, memory_exporter + ): + """Verify attributes propagate to different observation types.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id="user_123"): + # Create span + span = langfuse_client.start_observation(name="test-span") + span.end() + + # Create generation + generation = langfuse_client.start_observation( + as_type="generation", name="test-generation" + ) + generation.end() + + # Verify both observation types have user_id + span_data = self.get_span_by_name(memory_exporter, "test-span") + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + generation_data = self.get_span_by_name(memory_exporter, "test-generation") + self.verify_span_attribute( + generation_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + +class TestPropagateAttributesTiming(TestPropagateAttributesBase): + """Critical tests for early vs late propagation timing.""" + + def test_early_propagation_all_spans_covered( + self, langfuse_client, memory_exporter + ): + """Verify setting attributes early covers all child spans.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + # Set attributes BEFORE creating any children + with propagate_attributes(user_id="user_123"): + child1 = langfuse_client.start_observation(name="child-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-2") + child2.end() + + child3 = langfuse_client.start_observation(name="child-3") + child3.end() + + # Verify ALL children have user_id + for i in range(1, 4): + child_span = self.get_span_by_name(memory_exporter, f"child-{i}") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + def test_late_propagation_only_future_spans_covered( + self, langfuse_client, memory_exporter + ): + """Verify late propagation only affects spans created after context entry.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + # Create child1 BEFORE propagate_attributes + child1 = langfuse_client.start_observation(name="child-1") + child1.end() + + # NOW set attributes + with propagate_attributes(user_id="user_123"): + # Create child2 AFTER propagate_attributes + child2 = langfuse_client.start_observation(name="child-2") + child2.end() + + # Verify: child1 does NOT have user_id, child2 DOES + child1_span = self.get_span_by_name(memory_exporter, "child-1") + self.verify_missing_attribute( + child1_span, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + + child2_span = self.get_span_by_name(memory_exporter, "child-2") + self.verify_span_attribute( + child2_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + def test_current_span_gets_attributes(self, langfuse_client, memory_exporter): + """Verify the currently active span gets attributes when propagate_attributes is called.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + # Call propagate_attributes while parent-span is active + with propagate_attributes(user_id="user_123"): + pass + + # Verify parent span itself has the attribute + parent_span = self.get_span_by_name(memory_exporter, "parent-span") + self.verify_span_attribute( + parent_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + def test_spans_outside_context_unaffected(self, langfuse_client, memory_exporter): + """Verify spans created outside context don't get attributes.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + # Span before context + span1 = langfuse_client.start_observation(name="span-1") + span1.end() + + # Span inside context + with propagate_attributes(user_id="user_123"): + span2 = langfuse_client.start_observation(name="span-2") + span2.end() + + # Span after context + span3 = langfuse_client.start_observation(name="span-3") + span3.end() + + # Verify: only span2 has user_id + span1_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_missing_attribute( + span1_data, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + + span2_data = self.get_span_by_name(memory_exporter, "span-2") + self.verify_span_attribute( + span2_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + span3_data = self.get_span_by_name(memory_exporter, "span-3") + self.verify_missing_attribute( + span3_data, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + + +class TestPropagateAttributesValidation(TestPropagateAttributesBase): + """Tests for validation of propagated attribute values.""" + + def test_user_id_over_200_chars_dropped(self, langfuse_client, memory_exporter): + """Verify user_id over 200 characters is dropped with warning.""" + long_user_id = "x" * 201 + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id=long_user_id): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have user_id + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + + def test_session_id_over_200_chars_dropped(self, langfuse_client, memory_exporter): + """Verify session_id over 200 characters is dropped with warning.""" + long_session_id = "y" * 201 + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(session_id=long_session_id): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have session_id + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID + ) + + def test_metadata_value_over_200_chars_dropped( + self, langfuse_client, memory_exporter + ): + """Verify metadata values over 200 characters are dropped with warning.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata={"key": "z" * 201}): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have metadata.key + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute( + child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.key" + ) + + def test_exactly_200_chars_accepted(self, langfuse_client, memory_exporter): + """Verify exactly 200 characters is accepted (boundary test).""" + user_id_200 = "x" * 200 + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id=user_id_200): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child HAS user_id + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, user_id_200 + ) + + def test_201_chars_rejected(self, langfuse_client, memory_exporter): + """Verify 201 characters is rejected (boundary test).""" + user_id_201 = "x" * 201 + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id=user_id_201): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have user_id + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + + def test_non_string_user_id_dropped(self, langfuse_client, memory_exporter): + """Verify non-string user_id is dropped with warning.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id=12345): # type: ignore + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have user_id + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + + def test_mixed_valid_invalid_metadata(self, langfuse_client, memory_exporter): + """Verify mixed valid/invalid metadata - valid entries kept, invalid dropped.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={ + "valid_key": "valid_value", + "invalid_key": "x" * 201, # Too long + "another_valid": "ok", + } + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify: valid keys present, invalid key absent + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.valid_key", + "valid_value", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.another_valid", + "ok", + ) + self.verify_missing_attribute( + child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.invalid_key" + ) + + +class TestPropagateAttributesNesting(TestPropagateAttributesBase): + """Tests for nested propagate_attributes contexts.""" + + def test_nested_contexts_inner_overwrites(self, langfuse_client, memory_exporter): + """Verify inner context overwrites outer context values.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id="user1"): + # Create span in outer context + span1 = langfuse_client.start_observation(name="span-1") + span1.end() + + # Inner context with different user_id + with propagate_attributes(user_id="user2"): + span2 = langfuse_client.start_observation(name="span-2") + span2.end() + + # Verify: span1 has user1, span2 has user2 + span1_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span1_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user1" + ) + + span2_data = self.get_span_by_name(memory_exporter, "span-2") + self.verify_span_attribute( + span2_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user2" + ) + + def test_after_inner_context_outer_restored(self, langfuse_client, memory_exporter): + """Verify outer context is restored after exiting inner context.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id="user1"): + # Span in outer context + span1 = langfuse_client.start_observation(name="span-1") + span1.end() + + # Inner context + with propagate_attributes(user_id="user2"): + span2 = langfuse_client.start_observation(name="span-2") + span2.end() + + # Back to outer context + span3 = langfuse_client.start_observation(name="span-3") + span3.end() + + # Verify: span1 and span3 have user1, span2 has user2 + span1_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span1_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user1" + ) + + span2_data = self.get_span_by_name(memory_exporter, "span-2") + self.verify_span_attribute( + span2_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user2" + ) + + span3_data = self.get_span_by_name(memory_exporter, "span-3") + self.verify_span_attribute( + span3_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user1" + ) + + def test_nested_different_attributes(self, langfuse_client, memory_exporter): + """Verify nested contexts with different attributes merge correctly.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id="user1"): + # Inner context adds session_id + with propagate_attributes(session_id="session1"): + span = langfuse_client.start_observation(name="span-1") + span.end() + + # Verify: span has BOTH user_id and session_id + span_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user1" + ) + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session1" + ) + + def test_nested_metadata_merges_additively(self, langfuse_client, memory_exporter): + """Verify nested contexts merge metadata keys additively.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata={"env": "prod", "region": "us-east"}): + # Outer span should have outer metadata + outer_span = langfuse_client.start_observation(name="outer-span") + outer_span.end() + + # Inner context adds more metadata + with propagate_attributes( + metadata={"experiment": "A", "version": "2.0"} + ): + inner_span = langfuse_client.start_observation(name="inner-span") + inner_span.end() + + # Back to outer context + after_span = langfuse_client.start_observation(name="after-span") + after_span.end() + + # Verify: outer span has only outer metadata + outer_span_data = self.get_span_by_name(memory_exporter, "outer-span") + self.verify_span_attribute( + outer_span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "prod", + ) + self.verify_span_attribute( + outer_span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", + "us-east", + ) + self.verify_missing_attribute( + outer_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment" + ) + + # Verify: inner span has ALL metadata (merged) + inner_span_data = self.get_span_by_name(memory_exporter, "inner-span") + self.verify_span_attribute( + inner_span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "prod", + ) + self.verify_span_attribute( + inner_span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", + "us-east", + ) + self.verify_span_attribute( + inner_span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", + "A", + ) + self.verify_span_attribute( + inner_span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", + "2.0", + ) + + # Verify: after span has only outer metadata (inner context exited) + after_span_data = self.get_span_by_name(memory_exporter, "after-span") + self.verify_span_attribute( + after_span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "prod", + ) + self.verify_span_attribute( + after_span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", + "us-east", + ) + self.verify_missing_attribute( + after_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment" + ) + + def test_nested_metadata_inner_overwrites_conflicting_keys( + self, langfuse_client, memory_exporter + ): + """Verify nested contexts: inner metadata overwrites outer for same keys.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={"env": "staging", "version": "1.0", "region": "us-west"} + ): + # Inner context overwrites some keys + with propagate_attributes( + metadata={"env": "production", "experiment": "B"} + ): + span = langfuse_client.start_observation(name="span-1") + span.end() + + # Verify: inner values overwrite outer for conflicting keys + span_data = self.get_span_by_name(memory_exporter, "span-1") + + # Overwritten key + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "production", # Inner value wins + ) + + # Preserved keys from outer + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", + "1.0", # From outer + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", + "us-west", # From outer + ) + + # New key from inner + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", + "B", # From inner + ) + + def test_triple_nested_metadata_accumulates(self, langfuse_client, memory_exporter): + """Verify metadata accumulates across three levels of nesting.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata={"level": "1", "a": "outer"}): + with propagate_attributes(metadata={"level": "2", "b": "middle"}): + with propagate_attributes(metadata={"level": "3", "c": "inner"}): + span = langfuse_client.start_observation(name="deep-span") + span.end() + + # Verify: deepest span has all metadata with innermost level winning + span_data = self.get_span_by_name(memory_exporter, "deep-span") + + # Conflicting key: innermost wins + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.level", + "3", + ) + + # Unique keys from each level + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.a", + "outer", + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.b", + "middle", + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.c", + "inner", + ) + + def test_metadata_merge_with_empty_inner(self, langfuse_client, memory_exporter): + """Verify empty inner metadata dict doesn't clear outer metadata.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata={"key1": "value1", "key2": "value2"}): + # Inner context with empty metadata + with propagate_attributes(metadata={}): + span = langfuse_client.start_observation(name="span-1") + span.end() + + # Verify: outer metadata is preserved + span_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.key1", + "value1", + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.key2", + "value2", + ) + + def test_metadata_merge_preserves_user_session( + self, langfuse_client, memory_exporter + ): + """Verify metadata merging doesn't affect user_id/session_id.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + user_id="user1", + session_id="session1", + metadata={"outer": "value"}, + ): + with propagate_attributes(metadata={"inner": "value"}): + span = langfuse_client.start_observation(name="span-1") + span.end() + + # Verify: user_id and session_id are preserved, metadata merged + span_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user1" + ) + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session1" + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.outer", + "value", + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.inner", + "value", + ) + + +class TestPropagateAttributesEdgeCases(TestPropagateAttributesBase): + """Tests for edge cases and unusual scenarios.""" + + def test_propagate_attributes_with_no_args(self, langfuse_client, memory_exporter): + """Verify calling propagate_attributes() with no args doesn't error.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Should not crash, spans created normally + child_span = self.get_span_by_name(memory_exporter, "child-span") + assert child_span is not None + + def test_none_values_ignored(self, langfuse_client, memory_exporter): + """Verify None values are ignored without error.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id=None, session_id=None, metadata=None): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Should not crash, no attributes set + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + self.verify_missing_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID + ) + + def test_empty_metadata_dict(self, langfuse_client, memory_exporter): + """Verify empty metadata dict doesn't cause errors.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata={}): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Should not crash, no metadata attributes set + child_span = self.get_span_by_name(memory_exporter, "child-span") + assert child_span is not None + + def test_all_invalid_metadata_values(self, langfuse_client, memory_exporter): + """Verify all invalid metadata values results in no metadata attributes.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={ + "key1": "x" * 201, # Too long + "key2": "y" * 201, # Too long + } + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # No metadata attributes should be set + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute( + child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.key1" + ) + self.verify_missing_attribute( + child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.key2" + ) + + def test_propagate_with_no_active_span(self, langfuse_client, memory_exporter): + """Verify propagate_attributes works even with no active span.""" + # Call propagate_attributes without creating a parent span first + with propagate_attributes(user_id="user_123"): + # Now create a span + with langfuse_client.start_as_current_observation(name="span-1"): + pass + + # Should not crash, span should have user_id + span_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + +class TestPropagateAttributesFormat(TestPropagateAttributesBase): + """Tests for correct attribute formatting and naming.""" + + def test_user_id_uses_correct_attribute_name( + self, langfuse_client, memory_exporter + ): + """Verify user_id uses the correct OTel attribute name.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(user_id="user_123"): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + # Verify the exact attribute key is used + assert LangfuseOtelSpanAttributes.TRACE_USER_ID in child_span["attributes"] + assert ( + child_span["attributes"][LangfuseOtelSpanAttributes.TRACE_USER_ID] + == "user_123" + ) + + def test_session_id_uses_correct_attribute_name( + self, langfuse_client, memory_exporter + ): + """Verify session_id uses the correct OTel attribute name.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(session_id="session_abc"): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + # Verify the exact attribute key is used + assert LangfuseOtelSpanAttributes.TRACE_SESSION_ID in child_span["attributes"] + assert ( + child_span["attributes"][LangfuseOtelSpanAttributes.TRACE_SESSION_ID] + == "session_abc" + ) + + def test_metadata_keys_properly_prefixed(self, langfuse_client, memory_exporter): + """Verify metadata keys are properly prefixed with TRACE_METADATA.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={"experiment": "A", "version": "1.0", "env": "prod"} + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + attributes = child_span["attributes"] + + # Verify each metadata key is properly prefixed + expected_keys = [ + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + ] + + for key in expected_keys: + assert key in attributes, f"Expected key '{key}' not found in attributes" + + def test_multiple_metadata_keys_independent(self, langfuse_client, memory_exporter): + """Verify multiple metadata keys are stored as independent attributes.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata={"k1": "v1", "k2": "v2", "k3": "v3"}): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + attributes = child_span["attributes"] + + # Verify all three are separate attributes with correct values + assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k1"] == "v1" + assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k2"] == "v2" + assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k3"] == "v3" + + +class TestPropagateAttributesThreading(TestPropagateAttributesBase): + """Tests for propagate_attributes with ThreadPoolExecutor.""" + + @pytest.fixture(autouse=True) + def instrument_threading(self): + """Auto-instrument threading for all tests in this class.""" + instrumentor = ThreadingInstrumentor() + instrumentor.instrument() + yield + instrumentor.uninstrument() + + def test_propagation_with_threadpoolexecutor( + self, langfuse_client, memory_exporter + ): + """Verify attributes propagate from main thread to worker threads.""" + + def worker_function(span_name: str): + """Worker creates a span in thread pool.""" + span = langfuse_client.start_observation(name=span_name) + span.end() + return span_name + + with langfuse_client.start_as_current_observation(name="main-span"): + with propagate_attributes(user_id="main_user", session_id="main_session"): + # Execute work in thread pool + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [ + executor.submit(worker_function, f"worker-span-{i}") + for i in range(3) + ] + concurrent.futures.wait(futures) + + # Verify all worker spans have propagated attributes + for i in range(3): + worker_span = self.get_span_by_name(memory_exporter, f"worker-span-{i}") + self.verify_span_attribute( + worker_span, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "main_user", + ) + self.verify_span_attribute( + worker_span, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "main_session", + ) + + def test_propagation_isolated_between_threads( + self, langfuse_client, memory_exporter + ): + """Verify each thread's context is isolated from others.""" + + def create_trace_with_user(user_id: str): + """Create a trace with specific user_id.""" + with langfuse_client.start_as_current_observation(name=f"trace-{user_id}"): + with propagate_attributes(user_id=user_id): + span = langfuse_client.start_observation(name=f"span-{user_id}") + span.end() + + # Run two traces concurrently with different user_ids + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + future1 = executor.submit(create_trace_with_user, "user1") + future2 = executor.submit(create_trace_with_user, "user2") + concurrent.futures.wait([future1, future2]) + + # Verify each trace has the correct user_id (no mixing) + span1 = self.get_span_by_name(memory_exporter, "span-user1") + self.verify_span_attribute( + span1, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user1" + ) + + span2 = self.get_span_by_name(memory_exporter, "span-user2") + self.verify_span_attribute( + span2, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user2" + ) + + def test_nested_propagation_across_thread_boundary( + self, langfuse_client, memory_exporter + ): + """Verify nested spans across thread boundaries inherit attributes.""" + + def worker_creates_child(): + """Worker thread creates a child span.""" + child = langfuse_client.start_observation(name="worker-child-span") + child.end() + + with langfuse_client.start_as_current_observation(name="main-parent-span"): + with propagate_attributes(user_id="main_user"): + # Create span in main thread + main_child = langfuse_client.start_observation(name="main-child-span") + main_child.end() + + # Create span in worker thread + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(worker_creates_child) + future.result() + + # Verify both spans (main and worker) have user_id + main_child_span = self.get_span_by_name(memory_exporter, "main-child-span") + self.verify_span_attribute( + main_child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "main_user" + ) + + worker_child_span = self.get_span_by_name(memory_exporter, "worker-child-span") + self.verify_span_attribute( + worker_child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "main_user" + ) + + def test_worker_thread_can_override_propagated_attrs( + self, langfuse_client, memory_exporter + ): + """Verify worker thread can override propagated attributes.""" + + def worker_overrides_user(): + """Worker thread sets its own user_id.""" + with propagate_attributes(user_id="worker_user"): + span = langfuse_client.start_observation(name="worker-span") + span.end() + + with langfuse_client.start_as_current_observation(name="main-span"): + with propagate_attributes(user_id="main_user"): + # Create span in main thread + main_span = langfuse_client.start_observation(name="main-child-span") + main_span.end() + + # Worker overrides with its own user_id + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(worker_overrides_user) + future.result() + + # Verify: main span has main_user, worker span has worker_user + main_child = self.get_span_by_name(memory_exporter, "main-child-span") + self.verify_span_attribute( + main_child, LangfuseOtelSpanAttributes.TRACE_USER_ID, "main_user" + ) + + worker_span = self.get_span_by_name(memory_exporter, "worker-span") + self.verify_span_attribute( + worker_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "worker_user" + ) + + def test_multiple_workers_with_same_propagated_context( + self, langfuse_client, memory_exporter + ): + """Verify multiple workers all inherit same propagated context.""" + + def worker_function(worker_id: int): + """Worker creates a span.""" + span = langfuse_client.start_observation(name=f"worker-{worker_id}") + span.end() + + with langfuse_client.start_as_current_observation(name="main-span"): + with propagate_attributes(session_id="shared_session"): + # Submit 5 workers + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + futures = [executor.submit(worker_function, i) for i in range(5)] + concurrent.futures.wait(futures) + + # Verify all 5 workers have same session_id + for i in range(5): + worker_span = self.get_span_by_name(memory_exporter, f"worker-{i}") + self.verify_span_attribute( + worker_span, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "shared_session", + ) + + def test_concurrent_traces_with_different_attributes( + self, langfuse_client, memory_exporter + ): + """Verify concurrent traces with different attributes don't mix.""" + + def create_trace(trace_id: int): + """Create a trace with unique user_id.""" + with langfuse_client.start_as_current_observation(name=f"trace-{trace_id}"): + with propagate_attributes(user_id=f"user_{trace_id}"): + span = langfuse_client.start_observation(name=f"span-{trace_id}") + span.end() + + # Create 10 traces concurrently + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + futures = [executor.submit(create_trace, i) for i in range(10)] + concurrent.futures.wait(futures) + + # Verify each trace has its correct user_id (no mixing) + for i in range(10): + span = self.get_span_by_name(memory_exporter, f"span-{i}") + self.verify_span_attribute( + span, LangfuseOtelSpanAttributes.TRACE_USER_ID, f"user_{i}" + ) + + def test_exception_in_worker_preserves_context( + self, langfuse_client, memory_exporter + ): + """Verify exception in worker doesn't corrupt main thread context.""" + + def worker_raises_exception(): + """Worker creates span then raises exception.""" + span = langfuse_client.start_observation(name="worker-span") + span.end() + raise ValueError("Test exception") + + with langfuse_client.start_as_current_observation(name="main-span"): + with propagate_attributes(user_id="main_user"): + # Create span before worker + span1 = langfuse_client.start_observation(name="span-before") + span1.end() + + # Worker raises exception (catch it) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(worker_raises_exception) + try: + future.result() + except ValueError: + pass # Expected + + # Create span after exception + span2 = langfuse_client.start_observation(name="span-after") + span2.end() + + # Verify both main thread spans still have correct user_id + span_before = self.get_span_by_name(memory_exporter, "span-before") + self.verify_span_attribute( + span_before, LangfuseOtelSpanAttributes.TRACE_USER_ID, "main_user" + ) + + span_after = self.get_span_by_name(memory_exporter, "span-after") + self.verify_span_attribute( + span_after, LangfuseOtelSpanAttributes.TRACE_USER_ID, "main_user" + ) + + +class TestPropagateAttributesCrossTracer(TestPropagateAttributesBase): + """Tests for propagate_attributes with different OpenTelemetry tracers.""" + + def test_different_tracer_spans_get_attributes( + self, langfuse_client, memory_exporter, tracer_provider + ): + """Verify spans from different tracers get propagated attributes.""" + # Get a different tracer (not the Langfuse tracer) + other_tracer = tracer_provider.get_tracer("other-library", "1.0.0") + + with langfuse_client.start_as_current_observation(name="langfuse-parent"): + with propagate_attributes(user_id="user_123", session_id="session_abc"): + # Create span with Langfuse tracer + langfuse_span = langfuse_client.start_observation(name="langfuse-child") + langfuse_span.end() + + # Create span with different tracer + with other_tracer.start_as_current_span(name="other-library-span"): + pass + + # Verify both spans have the propagated attributes + langfuse_span_data = self.get_span_by_name(memory_exporter, "langfuse-child") + self.verify_span_attribute( + langfuse_span_data, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "user_123", + ) + self.verify_span_attribute( + langfuse_span_data, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "session_abc", + ) + + other_span_data = self.get_span_by_name(memory_exporter, "other-library-span") + self.verify_span_attribute( + other_span_data, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "user_123", + ) + self.verify_span_attribute( + other_span_data, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "session_abc", + ) + + def test_nested_spans_from_multiple_tracers( + self, langfuse_client, memory_exporter, tracer_provider + ): + """Verify nested spans from multiple tracers all get propagated attributes.""" + tracer_a = tracer_provider.get_tracer("library-a", "1.0.0") + tracer_b = tracer_provider.get_tracer("library-b", "2.0.0") + + with langfuse_client.start_as_current_observation(name="root"): + with propagate_attributes( + user_id="user_123", metadata={"experiment": "cross_tracer"} + ): + # Create nested spans from different tracers + with tracer_a.start_as_current_span(name="library-a-span"): + with tracer_b.start_as_current_span(name="library-b-span"): + langfuse_leaf = langfuse_client.start_observation( + name="langfuse-leaf" + ) + langfuse_leaf.end() + + # Verify all spans have the attributes + for span_name in ["library-a-span", "library-b-span", "langfuse-leaf"]: + span_data = self.get_span_by_name(memory_exporter, span_name) + self.verify_span_attribute( + span_data, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "user_123", + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", + "cross_tracer", + ) + + def test_other_tracer_span_before_propagate_context( + self, langfuse_client, memory_exporter, tracer_provider + ): + """Verify spans created before propagate_attributes don't get attributes.""" + other_tracer = tracer_provider.get_tracer("other-library", "1.0.0") + + with langfuse_client.start_as_current_observation(name="root"): + # Create span BEFORE propagate_attributes + with other_tracer.start_as_current_span(name="span-before"): + pass + + # NOW set attributes + with propagate_attributes(user_id="user_123"): + # Create span AFTER propagate_attributes + with other_tracer.start_as_current_span(name="span-after"): + pass + + # Verify: span-before does NOT have user_id, span-after DOES + span_before = self.get_span_by_name(memory_exporter, "span-before") + self.verify_missing_attribute( + span_before, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + + span_after = self.get_span_by_name(memory_exporter, "span-after") + self.verify_span_attribute( + span_after, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + def test_mixed_tracers_with_metadata( + self, langfuse_client, memory_exporter, tracer_provider + ): + """Verify metadata propagates correctly to spans from different tracers.""" + other_tracer = tracer_provider.get_tracer("instrumented-library", "1.0.0") + + with langfuse_client.start_as_current_observation(name="main"): + with propagate_attributes( + metadata={ + "env": "production", + "version": "2.0", + "feature_flag": "enabled", + } + ): + # Create spans from both tracers + langfuse_span = langfuse_client.start_observation( + name="langfuse-operation" + ) + langfuse_span.end() + + with other_tracer.start_as_current_span(name="library-operation"): + pass + + # Verify both spans have all metadata + for span_name in ["langfuse-operation", "library-operation"]: + span_data = self.get_span_by_name(memory_exporter, span_name) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "production", + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", + "2.0", + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.feature_flag", + "enabled", + ) + + def test_propagate_without_langfuse_parent( + self, langfuse_client, memory_exporter, tracer_provider + ): + """Verify propagate_attributes works even when parent span is from different tracer.""" + other_tracer = tracer_provider.get_tracer("other-library", "1.0.0") + + # Parent span is from different tracer + with other_tracer.start_as_current_span(name="other-parent"): + with propagate_attributes(user_id="user_123", session_id="session_xyz"): + # Create children from both tracers + with other_tracer.start_as_current_span(name="other-child"): + pass + + langfuse_child = langfuse_client.start_observation( + name="langfuse-child" + ) + langfuse_child.end() + + # Verify all spans have attributes (including non-Langfuse parent) + for span_name in ["other-parent", "other-child", "langfuse-child"]: + span_data = self.get_span_by_name(memory_exporter, span_name) + self.verify_span_attribute( + span_data, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "user_123", + ) + self.verify_span_attribute( + span_data, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "session_xyz", + ) + + def test_attributes_persist_across_tracer_changes( + self, langfuse_client, memory_exporter, tracer_provider + ): + """Verify attributes persist as execution moves between different tracers.""" + tracer_1 = tracer_provider.get_tracer("library-1", "1.0.0") + tracer_2 = tracer_provider.get_tracer("library-2", "1.0.0") + tracer_3 = tracer_provider.get_tracer("library-3", "1.0.0") + + with langfuse_client.start_as_current_observation(name="root"): + with propagate_attributes(user_id="persistent_user"): + # Bounce between different tracers + with tracer_1.start_as_current_span(name="step-1"): + pass + + with tracer_2.start_as_current_span(name="step-2"): + with tracer_3.start_as_current_span(name="step-3"): + pass + + langfuse_span = langfuse_client.start_observation(name="step-4") + langfuse_span.end() + + # Verify all steps have the user_id + for step_name in ["step-1", "step-2", "step-3", "step-4"]: + span_data = self.get_span_by_name(memory_exporter, step_name) + self.verify_span_attribute( + span_data, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "persistent_user", + ) + + +class TestPropagateAttributesAsync(TestPropagateAttributesBase): + """Tests for propagate_attributes with async/await.""" + + @pytest.mark.asyncio + async def test_async_propagation_basic(self, langfuse_client, memory_exporter): + """Verify attributes propagate in async context.""" + + async def async_operation(): + """Async function that creates a span.""" + span = langfuse_client.start_observation(name="async-span") + span.end() + + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes(user_id="async_user", session_id="async_session"): + await async_operation() + + # Verify async span has attributes + async_span = self.get_span_by_name(memory_exporter, "async-span") + self.verify_span_attribute( + async_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "async_user" + ) + self.verify_span_attribute( + async_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "async_session" + ) + + @pytest.mark.asyncio + async def test_async_nested_operations(self, langfuse_client, memory_exporter): + """Verify attributes propagate through nested async operations.""" + + async def level_3(): + span = langfuse_client.start_observation(name="level-3-span") + span.end() + + async def level_2(): + span = langfuse_client.start_observation(name="level-2-span") + span.end() + await level_3() + + async def level_1(): + span = langfuse_client.start_observation(name="level-1-span") + span.end() + await level_2() + + with langfuse_client.start_as_current_observation(name="root"): + with propagate_attributes( + user_id="nested_user", metadata={"level": "nested"} + ): + await level_1() + + # Verify all levels have attributes + for span_name in ["level-1-span", "level-2-span", "level-3-span"]: + span_data = self.get_span_by_name(memory_exporter, span_name) + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "nested_user" + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.level", + "nested", + ) + + @pytest.mark.asyncio + async def test_async_context_manager(self, langfuse_client, memory_exporter): + """Verify propagate_attributes works as context manager in async function.""" + with langfuse_client.start_as_current_observation(name="parent"): + # propagate_attributes supports both sync and async contexts via regular 'with' + with propagate_attributes(user_id="async_ctx_user"): + span = langfuse_client.start_observation(name="inside-async-ctx") + span.end() + + span_data = self.get_span_by_name(memory_exporter, "inside-async-ctx") + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "async_ctx_user" + ) + + @pytest.mark.asyncio + async def test_multiple_async_tasks_concurrent( + self, langfuse_client, memory_exporter + ): + """Verify context isolation between concurrent async tasks.""" + import asyncio + + async def create_trace_with_user(user_id: str): + """Create a trace with specific user_id.""" + with langfuse_client.start_as_current_observation(name=f"trace-{user_id}"): + with propagate_attributes(user_id=user_id): + await asyncio.sleep(0.001) # Simulate async work + span = langfuse_client.start_observation(name=f"span-{user_id}") + span.end() + + # Run multiple traces concurrently + await asyncio.gather( + create_trace_with_user("user1"), + create_trace_with_user("user2"), + create_trace_with_user("user3"), + ) + + # Verify each trace has correct user_id (no mixing) + for user_id in ["user1", "user2", "user3"]: + span_data = self.get_span_by_name(memory_exporter, f"span-{user_id}") + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, user_id + ) + + @pytest.mark.asyncio + async def test_async_with_sync_nested(self, langfuse_client, memory_exporter): + """Verify attributes propagate from async to sync code.""" + + def sync_operation(): + """Sync function called from async context.""" + span = langfuse_client.start_observation(name="sync-in-async") + span.end() + + async def async_operation(): + """Async function that calls sync code.""" + span1 = langfuse_client.start_observation(name="async-span") + span1.end() + sync_operation() + + with langfuse_client.start_as_current_observation(name="root"): + with propagate_attributes(user_id="mixed_user"): + await async_operation() + + # Verify both spans have attributes + async_span = self.get_span_by_name(memory_exporter, "async-span") + self.verify_span_attribute( + async_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "mixed_user" + ) + + sync_span = self.get_span_by_name(memory_exporter, "sync-in-async") + self.verify_span_attribute( + sync_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "mixed_user" + ) + + @pytest.mark.asyncio + async def test_async_exception_preserves_context( + self, langfuse_client, memory_exporter + ): + """Verify context is preserved even when async operation raises exception.""" + + async def failing_operation(): + """Async operation that raises exception.""" + span = langfuse_client.start_observation(name="span-before-error") + span.end() + raise ValueError("Test error") + + with langfuse_client.start_as_current_observation(name="root"): + with propagate_attributes(user_id="error_user"): + span1 = langfuse_client.start_observation(name="span-before-async") + span1.end() + + try: + await failing_operation() + except ValueError: + pass # Expected + + span2 = langfuse_client.start_observation(name="span-after-error") + span2.end() + + # Verify all spans have attributes + for span_name in ["span-before-async", "span-before-error", "span-after-error"]: + span_data = self.get_span_by_name(memory_exporter, span_name) + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "error_user" + ) + + @pytest.mark.asyncio + async def test_async_with_metadata(self, langfuse_client, memory_exporter): + """Verify metadata propagates correctly in async context.""" + + async def async_with_metadata(): + span = langfuse_client.start_observation(name="async-metadata-span") + span.end() + + with langfuse_client.start_as_current_observation(name="root"): + with propagate_attributes( + user_id="metadata_user", + metadata={"async": "true", "operation": "test"}, + ): + await async_with_metadata() + + span_data = self.get_span_by_name(memory_exporter, "async-metadata-span") + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "metadata_user" + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.async", + "true", + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.operation", + "test", + ) + + +class TestPropagateAttributesBaggage(TestPropagateAttributesBase): + """Tests for as_baggage=True parameter and OpenTelemetry baggage propagation.""" + + def test_baggage_is_set_when_as_baggage_true(self, langfuse_client): + """Verify baggage entries are created with correct keys when as_baggage=True.""" + from opentelemetry import baggage + from opentelemetry import context as otel_context + + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes( + user_id="user_123", + session_id="session_abc", + metadata={"env": "test", "version": "2.0"}, + as_baggage=True, + ): + # Get current context and inspect baggage + current_context = otel_context.get_current() + baggage_entries = baggage.get_all(context=current_context) + + # Verify baggage entries exist with correct keys + assert "langfuse_user_id" in baggage_entries + assert baggage_entries["langfuse_user_id"] == "user_123" + + assert "langfuse_session_id" in baggage_entries + assert baggage_entries["langfuse_session_id"] == "session_abc" + + assert "langfuse_metadata_env" in baggage_entries + assert baggage_entries["langfuse_metadata_env"] == "test" + + assert "langfuse_metadata_version" in baggage_entries + assert baggage_entries["langfuse_metadata_version"] == "2.0" + + def test_spans_receive_attributes_from_baggage( + self, langfuse_client, memory_exporter + ): + """Verify child spans get attributes when parent uses as_baggage=True.""" + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes( + user_id="baggage_user", + session_id="baggage_session", + metadata={"source": "baggage"}, + as_baggage=True, + ): + # Create child span + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child span has all attributes + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "baggage_user" + ) + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "baggage_session", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.source", + "baggage", + ) + + def test_baggage_disabled_by_default(self, langfuse_client): + """Verify as_baggage=False (default) doesn't create baggage entries.""" + from opentelemetry import baggage + from opentelemetry import context as otel_context + + from langfuse._client.propagation import LANGFUSE_TRACE_ID_BAGGAGE_KEY + + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes( + user_id="user_123", + session_id="session_abc", + ): + # Get current context and inspect baggage + current_context = otel_context.get_current() + baggage_entries = baggage.get_all(context=current_context) + user_baggage_entries = { + key: value + for key, value in baggage_entries.items() + if key != LANGFUSE_TRACE_ID_BAGGAGE_KEY + } + + assert user_baggage_entries == {} + + def test_metadata_key_with_user_id_substring_doesnt_collide( + self, langfuse_client, memory_exporter + ): + """Verify metadata key containing 'user_id' substring doesn't map to TRACE_USER_ID.""" + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes( + metadata={"user_info": "some_data", "user_id_copy": "another"}, + as_baggage=True, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + + # Should NOT have TRACE_USER_ID attribute + self.verify_missing_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID + ) + + # Should have metadata attributes with correct keys + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.user_info", + "some_data", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.user_id_copy", + "another", + ) + + def test_metadata_key_with_session_substring_doesnt_collide( + self, langfuse_client, memory_exporter + ): + """Verify metadata key containing 'session_id' substring doesn't map to TRACE_SESSION_ID.""" + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes( + metadata={"session_data": "value1", "session_id_backup": "value2"}, + as_baggage=True, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + + # Should NOT have TRACE_SESSION_ID attribute + self.verify_missing_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID + ) + + # Should have metadata attributes with correct keys + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.session_data", + "value1", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.session_id_backup", + "value2", + ) + + def test_metadata_keys_extract_correctly_from_baggage( + self, langfuse_client, memory_exporter + ): + """Verify metadata keys are correctly formatted in baggage and extracted back.""" + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes( + metadata={ + "env": "production", + "region": "us-west", + "experiment_id": "exp_123", + }, + as_baggage=True, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + + # All metadata should be under TRACE_METADATA prefix + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "production", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", + "us-west", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment_id", + "exp_123", + ) + + def test_baggage_and_context_both_propagate(self, langfuse_client, memory_exporter): + """Verify attributes propagate when both baggage and context mechanisms are active.""" + with langfuse_client.start_as_current_observation(name="parent"): + # Enable baggage + with propagate_attributes( + user_id="user_both", + session_id="session_both", + metadata={"source": "both"}, + as_baggage=True, + ): + # Create multiple levels of nesting + with langfuse_client.start_as_current_observation(name="middle"): + child = langfuse_client.start_observation(name="leaf") + child.end() + + # Verify all spans have attributes + for span_name in ["parent", "middle", "leaf"]: + span_data = self.get_span_by_name(memory_exporter, span_name) + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_both" + ) + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session_both" + ) + self.verify_span_attribute( + span_data, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.source", + "both", + ) + + def test_baggage_survives_context_isolation(self, langfuse_client, memory_exporter): + """Simulate cross-process scenario: baggage persists when context is detached/reattached.""" + from opentelemetry import context as otel_context + + # Step 1: Create context with baggage + with langfuse_client.start_as_current_observation(name="original-process"): + with propagate_attributes( + user_id="cross_process_user", + session_id="cross_process_session", + as_baggage=True, + ): + # Capture the context with baggage + context_with_baggage = otel_context.get_current() + + # Step 2: Simulate "remote" process by creating span in saved context + # This mimics what happens when receiving an HTTP request with baggage headers + token = otel_context.attach(context_with_baggage) + try: + with langfuse_client.start_as_current_observation(name="remote-process"): + child = langfuse_client.start_observation(name="remote-child") + child.end() + finally: + otel_context.detach(token) + + # Verify remote spans have the propagated attributes from baggage + remote_child = self.get_span_by_name(memory_exporter, "remote-child") + self.verify_span_attribute( + remote_child, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "cross_process_user", + ) + self.verify_span_attribute( + remote_child, + LangfuseOtelSpanAttributes.TRACE_SESSION_ID, + "cross_process_session", + ) + + +class TestPropagateAttributesVersion(TestPropagateAttributesBase): + """Tests for version parameter propagation.""" + + def test_version_propagates_to_child_spans(self, langfuse_client, memory_exporter): + """Verify version propagates to all child spans within context.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(version="v1.2.3"): + child1 = langfuse_client.start_observation(name="child-span-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-span-2") + child2.end() + + # Verify both children have version + child1_span = self.get_span_by_name(memory_exporter, "child-span-1") + self.verify_span_attribute( + child1_span, + LangfuseOtelSpanAttributes.VERSION, + "v1.2.3", + ) + + child2_span = self.get_span_by_name(memory_exporter, "child-span-2") + self.verify_span_attribute( + child2_span, + LangfuseOtelSpanAttributes.VERSION, + "v1.2.3", + ) + + def test_version_with_user_and_session(self, langfuse_client, memory_exporter): + """Verify version works together with user_id and session_id.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + user_id="user_123", + session_id="session_abc", + version="2.0.0", + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child has all attributes + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session_abc" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.VERSION, "2.0.0" + ) + + def test_version_with_metadata(self, langfuse_client, memory_exporter): + """Verify version works together with metadata.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + version="1.0.0", + metadata={"env": "production", "region": "us-east"}, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.VERSION, "1.0.0" + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "production", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", + "us-east", + ) + + def test_version_validation_over_200_chars(self, langfuse_client, memory_exporter): + """Verify version over 200 characters is dropped with warning.""" + long_version = "v" + "1.0.0" * 50 # Create a very long version string + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(version=long_version): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have version + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute(child_span, LangfuseOtelSpanAttributes.VERSION) + + def test_version_exactly_200_chars(self, langfuse_client, memory_exporter): + """Verify exactly 200 character version is accepted.""" + version_200 = "v" * 200 + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(version=version_200): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child HAS version + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.VERSION, version_200 + ) + + def test_version_nested_contexts_inner_overwrites( + self, langfuse_client, memory_exporter + ): + """Verify inner context overwrites outer version.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(version="1.0.0"): + # Create span in outer context + span1 = langfuse_client.start_observation(name="span-1") + span1.end() + + # Inner context with different version + with propagate_attributes(version="2.0.0"): + span2 = langfuse_client.start_observation(name="span-2") + span2.end() + + # Back to outer context + span3 = langfuse_client.start_observation(name="span-3") + span3.end() + + # Verify: span1 and span3 have version 1.0.0, span2 has 2.0.0 + span1_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span1_data, LangfuseOtelSpanAttributes.VERSION, "1.0.0" + ) + + span2_data = self.get_span_by_name(memory_exporter, "span-2") + self.verify_span_attribute( + span2_data, LangfuseOtelSpanAttributes.VERSION, "2.0.0" + ) + + span3_data = self.get_span_by_name(memory_exporter, "span-3") + self.verify_span_attribute( + span3_data, LangfuseOtelSpanAttributes.VERSION, "1.0.0" + ) + + def test_version_with_baggage(self, langfuse_client, memory_exporter): + """Verify version propagates through baggage.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + version="baggage_version", + user_id="user_123", + as_baggage=True, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child has version + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.VERSION, "baggage_version" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + + def test_version_semantic_versioning_formats( + self, langfuse_client, memory_exporter + ): + """Verify various semantic versioning formats work correctly.""" + test_versions = [ + "1.0.0", + "v2.3.4", + "1.0.0-alpha", + "2.0.0-beta.1", + "3.1.4-rc.2+build.123", + "0.1.0", + ] + + with langfuse_client.start_as_current_observation(name="parent-span"): + for idx, version in enumerate(test_versions): + with propagate_attributes(version=version): + span = langfuse_client.start_observation(name=f"span-{idx}") + span.end() + + # Verify all versions are correctly set + for idx, expected_version in enumerate(test_versions): + span_data = self.get_span_by_name(memory_exporter, f"span-{idx}") + self.verify_span_attribute( + span_data, LangfuseOtelSpanAttributes.VERSION, expected_version + ) + + def test_version_non_string_dropped(self, langfuse_client, memory_exporter): + """Verify non-string version is dropped with warning.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(version=123): # type: ignore + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have version + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute(child_span, LangfuseOtelSpanAttributes.VERSION) + + def test_version_propagates_to_grandchildren( + self, langfuse_client, memory_exporter + ): + """Verify version propagates through multiple levels of nesting.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(version="nested_v1"): + with langfuse_client.start_as_current_observation(name="child-span"): + grandchild = langfuse_client.start_observation( + name="grandchild-span" + ) + grandchild.end() + + # Verify all three levels have version + parent_span = self.get_span_by_name(memory_exporter, "parent-span") + child_span = self.get_span_by_name(memory_exporter, "child-span") + grandchild_span = self.get_span_by_name(memory_exporter, "grandchild-span") + + for span in [parent_span, child_span, grandchild_span]: + self.verify_span_attribute( + span, LangfuseOtelSpanAttributes.VERSION, "nested_v1" + ) + + @pytest.mark.asyncio + async def test_version_with_async(self, langfuse_client, memory_exporter): + """Verify version propagates in async context.""" + + async def async_operation(): + span = langfuse_client.start_observation(name="async-span") + span.end() + + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes(version="async_v1.0"): + await async_operation() + + async_span = self.get_span_by_name(memory_exporter, "async-span") + self.verify_span_attribute( + async_span, LangfuseOtelSpanAttributes.VERSION, "async_v1.0" + ) + + def test_version_attribute_key_format(self, langfuse_client, memory_exporter): + """Verify version uses correct attribute key format.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(version="key_test_v1"): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + attributes = child_span["attributes"] + + # Verify exact attribute key + assert LangfuseOtelSpanAttributes.VERSION in attributes + assert attributes[LangfuseOtelSpanAttributes.VERSION] == "key_test_v1" + + +class TestPropagateAttributesTags(TestPropagateAttributesBase): + """Tests for tags parameter propagation.""" + + def test_tags_propagate_to_child_spans(self, langfuse_client, memory_exporter): + """Verify tags propagate to all child spans within context.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(tags=["production", "api-v2", "critical"]): + child1 = langfuse_client.start_observation(name="child-span-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-span-2") + child2.end() + + # Verify both children have tags + child1_span = self.get_span_by_name(memory_exporter, "child-span-1") + self.verify_span_attribute( + child1_span, + LangfuseOtelSpanAttributes.TRACE_TAGS, + tuple(["production", "api-v2", "critical"]), + ) + + child2_span = self.get_span_by_name(memory_exporter, "child-span-2") + self.verify_span_attribute( + child2_span, + LangfuseOtelSpanAttributes.TRACE_TAGS, + tuple(["production", "api-v2", "critical"]), + ) + + def test_tags_with_single_tag(self, langfuse_client, memory_exporter): + """Verify single tag works correctly.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(tags=["experiment"]): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.TRACE_TAGS, + tuple(["experiment"]), + ) + + def test_empty_tags_list(self, langfuse_client, memory_exporter): + """Verify empty tags list is handled correctly.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(tags=[]): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # With empty list, tags should not be set + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute(child_span, LangfuseOtelSpanAttributes.TRACE_TAGS) + + def test_tags_with_user_and_session(self, langfuse_client, memory_exporter): + """Verify tags work together with user_id and session_id.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + user_id="user_123", + session_id="session_abc", + tags=["test", "debug"], + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child has all attributes + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session_abc" + ) + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.TRACE_TAGS, + tuple(["test", "debug"]), + ) + + def test_tags_with_metadata(self, langfuse_client, memory_exporter): + """Verify tags work together with metadata.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + tags=["experiment-a", "variant-1"], + metadata={"env": "staging"}, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.TRACE_TAGS, + tuple(["experiment-a", "variant-1"]), + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "staging", + ) + + def test_tags_validation_with_invalid_tag(self, langfuse_client, memory_exporter): + """Verify tags with one invalid entry drops all tags.""" + long_tag = "x" * 201 # Over 200 chars + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(tags=["valid_tag", long_tag]): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_TAGS, tuple(["valid_tag"]) + ) + + def test_tags_nested_contexts_inner_appends(self, langfuse_client, memory_exporter): + """Verify inner context appends to outer tags.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(tags=["outer", "tag1"]): + # Create span in outer context + span1 = langfuse_client.start_observation(name="span-1") + span1.end() + + # Inner context with more tags + with propagate_attributes(tags=["inner", "tag2"]): + span2 = langfuse_client.start_observation(name="span-2") + span2.end() + + # Back to outer context + span3 = langfuse_client.start_observation(name="span-3") + span3.end() + + # Verify: span1 and span3 have outer tags, span2 has inner tags + span1_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span1_data, LangfuseOtelSpanAttributes.TRACE_TAGS, tuple(["outer", "tag1"]) + ) + + span2_data = self.get_span_by_name(memory_exporter, "span-2") + self.verify_span_attribute( + span2_data, + LangfuseOtelSpanAttributes.TRACE_TAGS, + tuple( + [ + "outer", + "tag1", + "inner", + "tag2", + ] + ), + ) + + span3_data = self.get_span_by_name(memory_exporter, "span-3") + self.verify_span_attribute( + span3_data, LangfuseOtelSpanAttributes.TRACE_TAGS, tuple(["outer", "tag1"]) + ) + + def test_tags_with_baggage(self, langfuse_client, memory_exporter): + """Verify tags propagate through baggage.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + tags=["baggage_tag1", "baggage_tag2"], + as_baggage=True, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child has tags + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.TRACE_TAGS, + tuple(["baggage_tag1", "baggage_tag2"]), + ) + + def test_tags_propagate_to_grandchildren(self, langfuse_client, memory_exporter): + """Verify tags propagate through multiple levels of nesting.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(tags=["level1", "level2", "level3"]): + with langfuse_client.start_as_current_observation(name="child-span"): + grandchild = langfuse_client.start_observation( + name="grandchild-span" + ) + grandchild.end() + + # Verify all three levels have tags + parent_span = self.get_span_by_name(memory_exporter, "parent-span") + child_span = self.get_span_by_name(memory_exporter, "child-span") + grandchild_span = self.get_span_by_name(memory_exporter, "grandchild-span") + + for span in [parent_span, child_span, grandchild_span]: + self.verify_span_attribute( + span, + LangfuseOtelSpanAttributes.TRACE_TAGS, + tuple(["level1", "level2", "level3"]), + ) + + @pytest.mark.asyncio + async def test_tags_with_async(self, langfuse_client, memory_exporter): + """Verify tags propagate in async context.""" + + async def async_operation(): + span = langfuse_client.start_observation(name="async-span") + span.end() + + with langfuse_client.start_as_current_observation(name="parent"): + with propagate_attributes(tags=["async", "test"]): + await async_operation() + + async_span = self.get_span_by_name(memory_exporter, "async-span") + self.verify_span_attribute( + async_span, LangfuseOtelSpanAttributes.TRACE_TAGS, tuple(["async", "test"]) + ) + + def test_tags_attribute_key_format(self, langfuse_client, memory_exporter): + """Verify tags use correct attribute key format.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(tags=["key_test"]): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + attributes = child_span["attributes"] + + # Verify exact attribute key + assert LangfuseOtelSpanAttributes.TRACE_TAGS in attributes + assert attributes[LangfuseOtelSpanAttributes.TRACE_TAGS] == tuple(["key_test"]) + + +class TestPropagateAttributesExperiment(TestPropagateAttributesBase): + """Tests for experiment attribute propagation.""" + + @pytest.mark.asyncio + async def test_experiment_propagates_user_id_in_async_context( + self, langfuse_client, memory_exporter + ): + """Verify run_experiment keeps propagated attributes when called from async code.""" + import asyncio + + local_data = [{"input": "test input", "expected_output": "expected output"}] + + async def async_task(*, item, **kwargs): + await asyncio.sleep(0.001) + return f"processed: {item['input']}" + + with propagate_attributes(user_id="async-experiment-user"): + langfuse_client.run_experiment( + name="Async Experiment", + data=local_data, + task=async_task, + ) + + langfuse_client.flush() + + root_span = self.get_span_by_name(memory_exporter, "experiment-item-run") + self.verify_span_attribute( + root_span, + LangfuseOtelSpanAttributes.TRACE_USER_ID, + "async-experiment-user", + ) + + def test_experiment_attributes_propagate_without_dataset( + self, langfuse_client, memory_exporter + ): + """Test experiment attribute propagation with local data (no Langfuse dataset).""" + # Create local dataset with metadata + local_data = [ + { + "input": "test input 1", + "expected_output": "expected result 1", + "metadata": {"item_type": "test", "priority": "high"}, + }, + ] + + # Task function that creates child spans + def task_with_child_spans(*, item, **kwargs): + # Create child spans to verify propagation + child1 = langfuse_client.start_observation(name="child-span-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-span-2") + child2.end() + + return f"processed: {item.get('input') if isinstance(item, dict) else item.input}" + + # Run experiment with local data + experiment_metadata = {"version": "1.0", "model": "test-model"} + result = langfuse_client.run_experiment( + name="Test Experiment", + description="Test experiment description", + data=local_data, + task=task_with_child_spans, + metadata=experiment_metadata, + ) + + # Flush to ensure spans are exported + langfuse_client.flush() + + # Get the root span + root_spans = self.get_spans_by_name(memory_exporter, "experiment-item-run") + assert len(root_spans) >= 1, "Should have at least 1 root span" + first_root = root_spans[0] + + # Root-only attributes should be on root + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_DESCRIPTION, + "Test experiment description", + ) + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_EXPECTED_OUTPUT, + _serialize("expected result 1"), + ) + + # Propagated attributes should also be on root + experiment_id = first_root["attributes"][ + LangfuseOtelSpanAttributes.EXPERIMENT_ID + ] + assert result.experiment_id == experiment_id + experiment_item_id = first_root["attributes"][ + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ID + ] + root_observation_id = first_root["attributes"][ + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID + ] + + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_NAME, + result.run_name, + ) + for metadata_key, metadata_value in experiment_metadata.items(): + self.verify_span_attribute( + first_root, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", + metadata_value, + ) + + self.verify_span_attribute( + first_root, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.item_type", + "test", + ) + self.verify_span_attribute( + first_root, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.priority", + "high", + ) + + # Environment should be set to sdk-experiment + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ) + + # Dataset ID should not be set for local data + self.verify_missing_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_DATASET_ID, + ) + + # Verify child spans have propagated attributes but NOT root-only attributes + child_spans = self.get_spans_by_name( + memory_exporter, "child-span-1" + ) + self.get_spans_by_name(memory_exporter, "child-span-2") + + assert len(child_spans) >= 2, "Should have at least 2 child spans" + + for child_span in child_spans[:2]: # Check first item's children + # Propagated attributes should be present + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_ID, + experiment_id, + ) + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_NAME, + result.run_name, + ) + for metadata_key, metadata_value in experiment_metadata.items(): + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", + metadata_value, + ) + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ID, + experiment_item_id, + ) + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID, + root_observation_id, + ) + + # Environment should be propagated to children + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ) + + # Root-only attributes should NOT be present on children + self.verify_missing_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_DESCRIPTION, + ) + self.verify_missing_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_EXPECTED_OUTPUT, + ) + + # Dataset ID should not be set for local data + self.verify_missing_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_DATASET_ID, + ) + + def test_experiment_id_is_stable_across_local_items( + self, langfuse_client, memory_exporter + ): + """Test local experiments reuse one experiment ID across all items.""" + local_data = [ + {"input": "test input 1", "expected_output": "expected result 1"}, + {"input": "test input 2", "expected_output": "expected result 2"}, + ] + + result = langfuse_client.run_experiment( + name="Stable Local Experiment", + data=local_data, + task=lambda *, item, **kwargs: f"processed: {item['input']}", + ) + + langfuse_client.flush() + + root_spans = self.get_spans_by_name(memory_exporter, "experiment-item-run") + experiment_ids = { + span["attributes"][LangfuseOtelSpanAttributes.EXPERIMENT_ID] + for span in root_spans + } + + assert len(experiment_ids) == 1 + assert result.experiment_id == next(iter(experiment_ids)) + + def test_experiment_attributes_propagate_with_dataset( + self, langfuse_client, memory_exporter, monkeypatch + ): + """Test experiment attribute propagation with Langfuse dataset.""" + + # Mock the sync API used by run_experiment to create dataset run items + def mock_create_dataset_run_item(*args, **kwargs): + from langfuse.api import DatasetRunItem + + return DatasetRunItem( + id="mock-run-item-id", + dataset_run_id="mock-dataset-run-id-123", + dataset_run_name=kwargs.get("run_name", "Dataset Test"), + dataset_item_id=kwargs.get("dataset_item_id", "mock-item-id"), + trace_id="mock-trace-id", + observation_id=kwargs.get("observation_id"), + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + monkeypatch.setattr( + langfuse_client.api.dataset_run_items, + "create", + mock_create_dataset_run_item, + ) + + # Create a mock dataset with items + dataset_id = "test-dataset-id-456" + dataset_item_id = "test-dataset-item-id-789" + + mock_dataset = Dataset( + id=dataset_id, + name="Test Dataset", + description="Test dataset description", + project_id="test-project-id", + metadata={"test": "metadata"}, + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + mock_dataset_item = DatasetItem( + id=dataset_item_id, + status=DatasetStatus.ACTIVE, + input="Germany", + expected_output="Berlin", + metadata={"source": "dataset", "index": 0}, + source_trace_id=None, + source_observation_id=None, + dataset_id=dataset_id, + dataset_name="Test Dataset", + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + # Create dataset client with items + dataset = DatasetClient( + dataset=mock_dataset, + items=[mock_dataset_item], + langfuse_client=langfuse_client, + ) + + # Task with child spans + def task_with_children(*, item, **kwargs): + child1 = langfuse_client.start_observation(name="dataset-child-1") + child1.end() + + child2 = langfuse_client.start_observation(name="dataset-child-2") + child2.end() + + return f"Capital: {item.expected_output}" + + # Run experiment + experiment_metadata = {"dataset_version": "v2", "test_run": "true"} + result = dataset.run_experiment( + name="Dataset Test", + description="Dataset experiment description", + task=task_with_children, + metadata=experiment_metadata, + ) + + langfuse_client.flush() + + # Verify root has dataset-specific attributes + root_spans = self.get_spans_by_name(memory_exporter, "experiment-item-run") + assert len(root_spans) >= 1, "Should have at least 1 root span" + first_root = root_spans[0] + assert result.experiment_id == "mock-dataset-run-id-123" + + # Root-only attributes should be on root + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_DESCRIPTION, + "Dataset experiment description", + ) + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_EXPECTED_OUTPUT, + _serialize("Berlin"), + ) + + # Should have dataset ID (this is the key difference from local data) + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_DATASET_ID, + dataset_id, + ) + + # Should have the dataset item ID + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ID, + dataset_item_id, + ) + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_ID, + result.experiment_id, + ) + + # Should have experiment metadata + for metadata_key, metadata_value in experiment_metadata.items(): + self.verify_span_attribute( + first_root, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", + metadata_value, + ) + + # Environment should be set to sdk-experiment + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ) + + # Verify child spans have dataset-specific propagated attributes + child_spans = self.get_spans_by_name( + memory_exporter, "dataset-child-1" + ) + self.get_spans_by_name(memory_exporter, "dataset-child-2") + + assert len(child_spans) >= 2, "Should have at least 2 child spans" + + for child_span in child_spans[:2]: + # Dataset ID should be propagated to children + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_DATASET_ID, + dataset_id, + ) + + # Dataset item ID should be propagated + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ID, + dataset_item_id, + ) + + # Experiment metadata should be propagated + for metadata_key, metadata_value in experiment_metadata.items(): + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", + metadata_value, + ) + + # Item metadata should be propagated + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.source", + "dataset", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.index", + "0", + ) + + # Environment should be propagated to children + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ) + + # Root-only attributes should NOT be present on children + self.verify_missing_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_DESCRIPTION, + ) + self.verify_missing_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_EXPECTED_OUTPUT, + ) + + def test_experiment_attributes_propagate_to_nested_children( + self, langfuse_client, memory_exporter + ): + """Test experiment attributes propagate to deeply nested child spans.""" + local_data = [{"input": "test", "expected_output": "result"}] + + # Task with deeply nested spans + def task_with_nested_spans(*, item, **kwargs): + with langfuse_client.start_as_current_observation(name="child-span"): + with langfuse_client.start_as_current_observation( + name="grandchild-span" + ): + great_grandchild = langfuse_client.start_observation( + name="great-grandchild-span" + ) + great_grandchild.end() + + return "processed" + + result = langfuse_client.run_experiment( + name="Nested Test", + description="Nested test", + data=local_data, + task=task_with_nested_spans, + metadata={"depth": "test"}, + ) + + langfuse_client.flush() + + root_spans = self.get_spans_by_name(memory_exporter, "experiment-item-run") + first_root = root_spans[0] + experiment_id = first_root["attributes"][ + LangfuseOtelSpanAttributes.EXPERIMENT_ID + ] + root_observation_id = first_root["attributes"][ + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID + ] + + # Verify root has environment set + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ) + + # Verify all nested children have propagated attributes + for span_name in ["child-span", "grandchild-span", "great-grandchild-span"]: + span_data = self.get_span_by_name(memory_exporter, span_name) + + # Propagated attributes should be present + self.verify_span_attribute( + span_data, + LangfuseOtelSpanAttributes.EXPERIMENT_ID, + experiment_id, + ) + self.verify_span_attribute( + span_data, + LangfuseOtelSpanAttributes.EXPERIMENT_NAME, + result.run_name, + ) + self.verify_span_attribute( + span_data, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID, + root_observation_id, + ) + + # Environment should be propagated to all nested children + self.verify_span_attribute( + span_data, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ) + + # Root-only attributes should NOT be present + self.verify_missing_attribute( + span_data, + LangfuseOtelSpanAttributes.EXPERIMENT_DESCRIPTION, + ) + self.verify_missing_attribute( + span_data, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_EXPECTED_OUTPUT, + ) + + def test_experiment_metadata_merging(self, langfuse_client, memory_exporter): + """Test that experiment metadata and item metadata are both propagated correctly.""" + from langfuse._client.attributes import _serialize + + # Rich metadata + experiment_metadata = { + "experiment_type": "A/B test", + "model_version": "2.0", + "temperature": 0.7, + } + item_metadata = { + "item_category": "finance", + "difficulty": "hard", + "language": "en", + } + + local_data = [ + { + "input": "test", + "expected_output": {"status": "success"}, + "metadata": item_metadata, + } + ] + + def task_with_child(*, item, **kwargs): + child = langfuse_client.start_observation(name="metadata-child") + child.end() + return "result" + + langfuse_client.run_experiment( + name="Metadata Test", + description="Metadata test", + data=local_data, + task=task_with_child, + metadata=experiment_metadata, + ) + + langfuse_client.flush() + + # Verify root span has environment set + root_span = self.get_span_by_name(memory_exporter, "experiment-item-run") + self.verify_span_attribute( + root_span, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ) + + # Verify child span has both experiment and item metadata propagated + child_span = self.get_span_by_name(memory_exporter, "metadata-child") + + # Verify experiment metadata is flattened and propagated + for metadata_key, metadata_value in experiment_metadata.items(): + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", + _serialize(metadata_value), + ) + + # Verify item metadata is flattened and propagated + for metadata_key, metadata_value in item_metadata.items(): + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.{metadata_key}", + _serialize(metadata_value), + ) + + # Verify environment is propagated to child + self.verify_span_attribute( + child_span, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, + ) + + def test_experiment_metadata_values_are_validated_individually( + self, langfuse_client, memory_exporter, caplog + ): + """Experiment metadata is flattened so large combined dicts still propagate.""" + + caplog.set_level("WARNING", logger="langfuse") + + experiment_metadata = { + "job_name": "j" * 150, + "build_url": "b" * 150, + "mode": "offline", + } + + local_data = [{"input": "test", "expected_output": "success"}] + + def task_with_child(*, item, **kwargs): + child = langfuse_client.start_observation(name="large-metadata-child") + child.end() + return "result" + + langfuse_client.run_experiment( + name="Large Metadata Test", + data=local_data, + task=task_with_child, + metadata=experiment_metadata, + ) + + langfuse_client.flush() + + child_span = self.get_span_by_name(memory_exporter, "large-metadata-child") + + for metadata_key, metadata_value in experiment_metadata.items(): + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", + metadata_value, + ) + + self.verify_missing_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, + ) + assert "experiment_metadata' value is over 200 characters" not in caplog.text + + +class TestPropagateAttributesTraceName(TestPropagateAttributesBase): + """Tests for trace_name parameter propagation.""" + + def test_trace_name_propagates_to_child_spans( + self, langfuse_client, memory_exporter + ): + """Verify trace_name propagates to all child spans within context.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(trace_name="my-trace-name"): + child1 = langfuse_client.start_observation(name="child-span-1") + child1.end() + + child2 = langfuse_client.start_observation(name="child-span-2") + child2.end() + + # Verify both children have trace_name + child1_span = self.get_span_by_name(memory_exporter, "child-span-1") + self.verify_span_attribute( + child1_span, + LangfuseOtelSpanAttributes.TRACE_NAME, + "my-trace-name", + ) + + child2_span = self.get_span_by_name(memory_exporter, "child-span-2") + self.verify_span_attribute( + child2_span, + LangfuseOtelSpanAttributes.TRACE_NAME, + "my-trace-name", + ) + + def test_trace_name_propagates_to_grandchildren( + self, langfuse_client, memory_exporter + ): + """Verify trace_name propagates through multiple levels of nesting.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(trace_name="nested-trace"): + with langfuse_client.start_as_current_observation(name="child-span"): + grandchild = langfuse_client.start_observation( + name="grandchild-span" + ) + grandchild.end() + + # Verify all three levels have trace_name + parent_span = self.get_span_by_name(memory_exporter, "parent-span") + child_span = self.get_span_by_name(memory_exporter, "child-span") + grandchild_span = self.get_span_by_name(memory_exporter, "grandchild-span") + + for span in [parent_span, child_span, grandchild_span]: + self.verify_span_attribute( + span, LangfuseOtelSpanAttributes.TRACE_NAME, "nested-trace" + ) + + def test_trace_name_with_user_and_session(self, langfuse_client, memory_exporter): + """Verify trace_name works together with user_id and session_id.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + user_id="user_123", + session_id="session_abc", + trace_name="combined-trace", + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child has all attributes + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session_abc" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, "combined-trace" + ) + + def test_trace_name_with_version(self, langfuse_client, memory_exporter): + """Verify trace_name works together with version.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + trace_name="versioned-trace", + version="1.0.0", + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, "versioned-trace" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.VERSION, "1.0.0" + ) + + def test_trace_name_with_metadata(self, langfuse_client, memory_exporter): + """Verify trace_name works together with metadata.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + trace_name="metadata-trace", + metadata={"env": "production", "region": "us-east"}, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, "metadata-trace" + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "production", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", + "us-east", + ) + + def test_trace_name_validation_over_200_chars( + self, langfuse_client, memory_exporter + ): + """Verify trace_name over 200 characters is dropped with warning.""" + long_name = "trace-" + "a" * 200 # Create a very long trace name + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(trace_name=long_name): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have trace_name + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute(child_span, LangfuseOtelSpanAttributes.TRACE_NAME) + + def test_trace_name_exactly_200_chars(self, langfuse_client, memory_exporter): + """Verify exactly 200 character trace_name is accepted.""" + trace_name_200 = "t" * 200 + + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(trace_name=trace_name_200): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child HAS trace_name + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, trace_name_200 + ) + + def test_trace_name_nested_contexts_inner_overwrites( + self, langfuse_client, memory_exporter + ): + """Verify inner context overwrites outer trace_name.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(trace_name="outer-trace"): + # Create span in outer context + span1 = langfuse_client.start_observation(name="span-1") + span1.end() + + # Inner context with different trace_name + with propagate_attributes(trace_name="inner-trace"): + span2 = langfuse_client.start_observation(name="span-2") + span2.end() + + # Back to outer context + span3 = langfuse_client.start_observation(name="span-3") + span3.end() + + # Verify: span1 and span3 have outer-trace, span2 has inner-trace + span1_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span1_data, LangfuseOtelSpanAttributes.TRACE_NAME, "outer-trace" + ) + + span2_data = self.get_span_by_name(memory_exporter, "span-2") + self.verify_span_attribute( + span2_data, LangfuseOtelSpanAttributes.TRACE_NAME, "inner-trace" + ) + + span3_data = self.get_span_by_name(memory_exporter, "span-3") + self.verify_span_attribute( + span3_data, LangfuseOtelSpanAttributes.TRACE_NAME, "outer-trace" + ) + + def test_trace_name_sets_on_current_span(self, langfuse_client, memory_exporter): + """Verify trace_name is set on the current span when entering context.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(trace_name="current-trace"): + pass # Just enter and exit context + + # Verify parent span has trace_name set + parent_span = self.get_span_by_name(memory_exporter, "parent-span") + self.verify_span_attribute( + parent_span, LangfuseOtelSpanAttributes.TRACE_NAME, "current-trace" + ) + + def test_trace_name_non_string_dropped(self, langfuse_client, memory_exporter): + """Verify non-string trace_name is dropped with warning.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(trace_name=123): # type: ignore + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child does NOT have trace_name + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute(child_span, LangfuseOtelSpanAttributes.TRACE_NAME) + + def test_trace_name_with_baggage(self, langfuse_client, memory_exporter): + """Verify trace_name propagates through baggage.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + trace_name="baggage-trace", + user_id="user_123", + as_baggage=True, + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child has trace_name + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, "baggage-trace" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) diff --git a/tests/unit/test_resource_manager.py b/tests/unit/test_resource_manager.py new file mode 100644 index 000000000..d0880dcd6 --- /dev/null +++ b/tests/unit/test_resource_manager.py @@ -0,0 +1,200 @@ +"""Test the LangfuseResourceManager and get_client() function.""" + +from queue import Queue +from types import SimpleNamespace +from typing import Sequence +from unittest.mock import Mock + +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +from langfuse import Langfuse +from langfuse._client.get_client import get_client +from langfuse._client.resource_manager import LangfuseResourceManager +from langfuse._task_manager.media_manager import MediaManager +from langfuse._task_manager.media_upload_consumer import MediaUploadConsumer +from langfuse._task_manager.score_ingestion_consumer import ScoreIngestionConsumer + + +class NoOpSpanExporter(SpanExporter): + """Minimal exporter used to verify configuration propagation.""" + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + return SpanExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + +def test_get_client_preserves_all_settings(monkeypatch): + """Test that get_client() preserves environment and all client settings.""" + with LangfuseResourceManager._lock: + LangfuseResourceManager._instances.clear() + + monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-comprehensive-default") + monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-comprehensive-default") + monkeypatch.setenv("LANGFUSE_BASE_URL", "http://localhost:3000") + + def should_export(span): + return span.name != "drop" + + span_exporter = NoOpSpanExporter() + + settings = { + "public_key": "pk-comprehensive", + "secret_key": "sk-comprehensive", + "environment": "test-env", + "release": "v1.2.3", + "timeout": 30, + "flush_at": 100, + "sample_rate": 0.8, + "should_export_span": should_export, + "additional_headers": {"X-Custom": "value"}, + "span_exporter": span_exporter, + } + + original_client = Langfuse(**settings) + retrieved_client = get_client() + + assert retrieved_client._environment == settings["environment"] + assert retrieved_client._release == settings["release"] + + assert retrieved_client._resources is not None + rm = retrieved_client._resources + assert rm.environment == settings["environment"] + assert rm.timeout == settings["timeout"] + assert rm.sample_rate == settings["sample_rate"] + assert rm.should_export_span is should_export + assert rm.additional_headers == settings["additional_headers"] + assert rm.span_exporter is span_exporter + + original_client.shutdown() + + +def test_get_client_multiple_clients_preserve_different_settings(): + """Test that get_client() preserves different settings for multiple clients.""" + + def should_export_a(span): + return span.name.startswith("a") + + def should_export_b(span): + return span.name.startswith("b") + + exporter_a = NoOpSpanExporter() + exporter_b = NoOpSpanExporter() + + # Settings for client A + settings_a = { + "public_key": "pk-comprehensive-a", + "secret_key": "sk-comprehensive-a", + "environment": "env-a", + "release": "release-a", + "timeout": 10, + "sample_rate": 0.5, + "should_export_span": should_export_a, + "span_exporter": exporter_a, + } + + # Settings for client B + settings_b = { + "public_key": "pk-comprehensive-b", + "secret_key": "sk-comprehensive-b", + "environment": "env-b", + "release": "release-b", + "timeout": 20, + "sample_rate": 0.9, + "should_export_span": should_export_b, + "span_exporter": exporter_b, + } + + client_a = Langfuse(**settings_a) + client_b = Langfuse(**settings_b) + + # Get clients via get_client() + retrieved_a = get_client(public_key="pk-comprehensive-a") + retrieved_b = get_client(public_key="pk-comprehensive-b") + + # Verify each client preserves its own settings + assert retrieved_a._environment == settings_a["environment"] + assert retrieved_b._environment == settings_b["environment"] + + if retrieved_a._resources and retrieved_b._resources: + assert retrieved_a._resources.timeout == settings_a["timeout"] + assert retrieved_b._resources.timeout == settings_b["timeout"] + assert retrieved_a._resources.sample_rate == settings_a["sample_rate"] + assert retrieved_b._resources.sample_rate == settings_b["sample_rate"] + assert retrieved_a._resources.release == settings_a["release"] + assert retrieved_b._resources.release == settings_b["release"] + assert retrieved_a._resources.should_export_span is should_export_a + assert retrieved_b._resources.should_export_span is should_export_b + assert retrieved_a._resources.span_exporter is exporter_a + assert retrieved_b._resources.span_exporter is exporter_b + + client_a.shutdown() + client_b.shutdown() + + +def test_score_ingestion_consumer_pause_wakes_blocked_thread(): + consumer = ScoreIngestionConsumer( + ingestion_queue=Queue(), + identifier=0, + client=Mock(), + public_key="pk-test", + flush_interval=30, + ) + + consumer.start() + consumer.pause() + consumer.join(timeout=0.5) + + assert not consumer.is_alive() + + +def test_media_upload_consumer_signal_shutdown_wakes_blocked_thread(): + media_manager = MediaManager( + api_client=Mock(), + httpx_client=Mock(), + media_upload_queue=Queue(), + ) + consumer = MediaUploadConsumer(identifier=0, media_manager=media_manager) + + consumer.start() + consumer.pause() + media_manager.signal_shutdown() + consumer.join(timeout=0.5) + + assert not consumer.is_alive() + + +def test_stop_and_join_consumer_threads_broadcasts_media_shutdown_after_pausing_all(): + events = [] + + class FakeConsumer: + def __init__(self, identifier): + self._identifier = identifier + + def pause(self): + events.append(("pause", self._identifier)) + + def join(self): + events.append(("join", self._identifier)) + + class FakeMediaManager: + def signal_shutdown(self, *, count): + events.append(("signal_shutdown", count)) + + fake_resource_manager = SimpleNamespace( + _media_upload_consumers=[FakeConsumer(0), FakeConsumer(1)], + _ingestion_consumers=[], + _media_manager=FakeMediaManager(), + ) + + LangfuseResourceManager._stop_and_join_consumer_threads(fake_resource_manager) + + assert events == [ + ("pause", 0), + ("pause", 1), + ("signal_shutdown", 2), + ("join", 0), + ("join", 1), + ] diff --git a/tests/test_serializer.py b/tests/unit/test_serializer.py similarity index 53% rename from tests/test_serializer.py rename to tests/unit/test_serializer.py index e9f1277bd..f4c8dde86 100644 --- a/tests/test_serializer.py +++ b/tests/unit/test_serializer.py @@ -6,6 +6,7 @@ from pathlib import Path from uuid import UUID +import pytest from pydantic import BaseModel from langfuse._utils.serializer import ( @@ -164,6 +165,12 @@ def test_none(): assert serializer.encode(None) == "null" +def test_infinity_floats(): + serializer = EventSerializer() + assert serializer.encode(float("inf")) == '"Infinity"' + assert serializer.encode(float("-inf")) == '"-Infinity"' + + def test_slots(): class SlotClass: __slots__ = ["field"] @@ -176,36 +183,124 @@ def __init__(self): assert json.loads(serializer.encode(obj)) == {"field": "value"} -def test_numpy_float32(): - import numpy as np +def test_deeply_nested_object_does_not_hang(): + class Inner: + def __init__(self): + self.lock = threading.Lock() + self.value = "deep" + + class Connection: + def __init__(self): + self._inner = Inner() + self._pool = [Inner() for _ in range(3)] + + class Client: + def __init__(self): + self._connection = Connection() + self._config = {"key": "value"} + + class Platform: + def __init__(self): + self._client = Client() + + obj = {"args": (Platform(),), "kwargs": {}} + serializer = EventSerializer() + result = serializer.encode(obj) + + # Must complete without hanging and produce valid JSON + parsed = json.loads(result) + assert "args" in parsed + + +def test_max_depth_returns_type_name(): + class Level: + def __init__(self, child=None): + self.child = child + + # Build a chain deeper than _MAX_DEPTH + obj = None + for _ in range(EventSerializer._MAX_DEPTH + 10): + obj = Level(child=obj) - data = np.float32(1.0) serializer = EventSerializer() + result = json.loads(serializer.encode(obj)) + + # Walk down the chain — at some point it should be truncated to "Level" + node = result + found_truncation = False + while isinstance(node, dict) and "child" in node: + if node["child"] == "Level" or node["child"] == "": + found_truncation = True + break + node = node["child"] - assert serializer.encode(data) == "1.0" + assert found_truncation, "Expected depth limit to truncate deep nesting" -def test_numpy_arrays(): - import numpy as np +def test_deeply_nested_slots_object_is_truncated(): + class SlotLevel: + __slots__ = ["child"] + + def __init__(self, child=None): + self.child = child + + obj = None + for _ in range(EventSerializer._MAX_DEPTH + 10): + obj = SlotLevel(child=obj) serializer = EventSerializer() + result = json.loads(serializer.encode(obj)) + + # Walk the nested structure and verify it terminates + node = result + depth = 0 + while isinstance(node, dict): + depth += 1 + if "child" in node: + node = node["child"] + else: + break - # Test 1D array - arr_1d = np.array([1, 2, 3]) - assert json.loads(serializer.encode(arr_1d)) == [1, 2, 3] + assert EventSerializer._MAX_DEPTH - 2 <= depth <= EventSerializer._MAX_DEPTH + 2, ( + f"Nesting depth {depth} not near _MAX_DEPTH ({EventSerializer._MAX_DEPTH}) — " + "serializer truncated too early or too late" + ) - # Test 2D array - arr_2d = np.array([[1, 2], [3, 4]]) - assert json.loads(serializer.encode(arr_2d)) == [[1, 2], [3, 4]] - # Test float array - arr_float = np.array([1.1, 2.2, 3.3]) - assert json.loads(serializer.encode(arr_float)) == [1.1, 2.2, 3.3] +def test_deeply_nested_dict_preserves_keys_at_depth_boundary(monkeypatch): + monkeypatch.setattr(EventSerializer, "_MAX_DEPTH", 3) - # Test empty array - arr_empty = np.array([]) - assert json.loads(serializer.encode(arr_empty)) == [] + input_obj = {"a": {"b": {"c": "leaf"}}} + expected = {"a": {"b": ""}} + + serializer = EventSerializer() + result = json.loads(serializer.encode(input_obj)) + + assert result == expected + + +class _Color(Enum): + RED = "red" + NUMERIC = 7 + + +@pytest.mark.parametrize( + "input_obj, expected", + [ + ( + {datetime(2024, 1, 1, tzinfo=timezone.utc): "v"}, + {"2024-01-01T00:00:00Z": "v"}, + ), + ( + {UUID("12345678-1234-5678-1234-567812345678"): "v"}, + {"12345678-1234-5678-1234-567812345678": "v"}, + ), + ({_Color.RED: "v"}, {"red": "v"}), + ({_Color.NUMERIC: "v"}, {"7": "v"}), + ], + ids=["datetime", "uuid", "enum_str_value", "enum_int_value"], +) +def test_dict_with_non_string_keys_is_serialized(input_obj, expected): + result = json.loads(EventSerializer().encode(input_obj)) - # Test mixed types that numpy can handle - arr_mixed = np.array([1, 2.5, 3]) - assert json.loads(serializer.encode(arr_mixed)) == [1.0, 2.5, 3.0] + assert result == expected diff --git a/tests/unit/test_span_filter.py b/tests/unit/test_span_filter.py new file mode 100644 index 000000000..cf9965d14 --- /dev/null +++ b/tests/unit/test_span_filter.py @@ -0,0 +1,165 @@ +"""Tests for span filter predicates used by the Langfuse span processor.""" + +from types import SimpleNamespace +from typing import Any, Optional + +import pytest + +from langfuse.span_filter import ( + KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES, + is_default_export_span, + is_genai_span, + is_known_llm_instrumentor, + is_langfuse_span, +) + + +def _make_span( + *, + scope_name: Optional[str] = None, + attributes: Optional[dict[Any, Any]] = None, +): + scope = None if scope_name is None else SimpleNamespace(name=scope_name) + return SimpleNamespace(instrumentation_scope=scope, attributes=attributes) + + +def test_is_langfuse_span_true(): + """Return true for a Langfuse SDK instrumentation scope.""" + assert is_langfuse_span(_make_span(scope_name="langfuse-sdk")) is True + + +def test_is_langfuse_span_false(): + """Return false for non-Langfuse instrumentation scopes.""" + assert is_langfuse_span(_make_span(scope_name="other-lib")) is False + + +def test_is_langfuse_span_no_scope(): + """Return false when instrumentation scope is missing.""" + assert is_langfuse_span(_make_span(scope_name=None)) is False + + +def test_is_genai_span_with_genai_attributes(): + """Return true when span attributes include a gen_ai.* key.""" + assert ( + is_genai_span( + _make_span( + attributes={"gen_ai.request.model": "gpt-4o", "http.method": "POST"} + ) + ) + is True + ) + + +def test_is_genai_span_ignores_non_string_keys(): + """Ignore non-string keys when checking gen_ai.* attributes.""" + assert ( + is_genai_span(_make_span(attributes={1: "value", "http.method": "POST"})) + is False + ) + + +def test_is_genai_span_no_attributes(): + """Return false when attributes are missing.""" + assert is_genai_span(_make_span(attributes=None)) is False + + +def test_is_known_llm_instrumentor_exact_match(): + """Return true for exact allowlisted scope names.""" + assert is_known_llm_instrumentor(_make_span(scope_name="ai")) is True + + +def test_is_known_llm_instrumentor_prefix_match(): + """Return true for allowlisted namespace scope descendants.""" + assert ( + is_known_llm_instrumentor( + _make_span(scope_name="openinference.instrumentation.agno.worker") + ) + is True + ) + + +@pytest.mark.parametrize( + "scope_name", + [ + "autogen-core", + "opentelemetry.instrumentation.agno", + "opentelemetry.instrumentation.alephalpha", + "opentelemetry.instrumentation.bedrock", + "opentelemetry.instrumentation.cohere", + "opentelemetry.instrumentation.crewai", + "opentelemetry.instrumentation.google_generativeai", + "opentelemetry.instrumentation.groq", + "opentelemetry.instrumentation.haystack", + "opentelemetry.instrumentation.langchain", + "opentelemetry.instrumentation.llamaindex", + "opentelemetry.instrumentation.mistralai", + "opentelemetry.instrumentation.ollama", + "opentelemetry.instrumentation.openai", + "opentelemetry.instrumentation.openai.v1", + "opentelemetry.instrumentation.openai_agents", + "opentelemetry.instrumentation.openai_v2", + "opentelemetry.instrumentation.replicate", + "opentelemetry.instrumentation.sagemaker", + "opentelemetry.instrumentation.together", + "opentelemetry.instrumentation.transformers", + "opentelemetry.instrumentation.vertexai", + "opentelemetry.instrumentation.voyageai", + "opentelemetry.instrumentation.watsonx", + "opentelemetry.instrumentation.writer", + "pydantic-ai", + ], +) +def test_is_known_llm_instrumentor_researched_scope_names(scope_name): + """Return true for researched OpenTelemetry LLM instrumentation scopes.""" + assert is_known_llm_instrumentor(_make_span(scope_name=scope_name)) is True + + +def test_known_llm_instrumentor_does_not_include_vector_store_only_scopes(): + """Keep vector DB-only instrumentors out of the default LLM scope allowlist.""" + vector_store_scopes = { + "opentelemetry.instrumentation.chromadb", + "opentelemetry.instrumentation.lancedb", + "opentelemetry.instrumentation.milvus", + "opentelemetry.instrumentation.pinecone", + "opentelemetry.instrumentation.qdrant", + "opentelemetry.instrumentation.weaviate", + } + + assert vector_store_scopes.isdisjoint(KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES) + + +def test_is_known_llm_instrumentor_unknown(): + """Return false for unknown instrumentation scopes.""" + assert is_known_llm_instrumentor(_make_span(scope_name="unknown.scope")) is False + + +def test_is_default_export_span_langfuse(): + """Export Langfuse spans with the default filter.""" + assert is_default_export_span(_make_span(scope_name="langfuse-sdk")) is True + + +def test_is_default_export_span_genai(): + """Export gen_ai spans with the default filter.""" + assert ( + is_default_export_span( + _make_span( + scope_name="unknown.scope", attributes={"gen_ai.prompt": "hello"} + ) + ) + is True + ) + + +def test_is_default_export_span_known_scope(): + """Export known instrumentation scopes with the default filter.""" + assert is_default_export_span(_make_span(scope_name="langsmith")) is True + + +def test_is_default_export_span_rejects_unknown(): + """Reject unknown scopes without gen_ai attributes in default filter.""" + assert ( + is_default_export_span( + _make_span(scope_name="unknown.scope", attributes={"http.method": "GET"}) + ) + is False + ) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 000000000..968bcb91b --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,271 @@ +"""Test suite for utility functions in langfuse._client.utils module.""" + +import asyncio +import contextvars +import threading +from unittest import mock + +import pytest + +from langfuse._client.utils import run_async_safely + + +class TestRunAsyncSafely: + """Test suite for the run_async_safely function.""" + + def test_run_sync_context_simple(self): + """Test run_async_safely in sync context with simple coroutine.""" + + async def simple_coro(): + await asyncio.sleep(0.01) + return "hello" + + result = run_async_safely(simple_coro()) + assert result == "hello" + + def test_run_sync_context_with_value(self): + """Test run_async_safely in sync context with parameter passing.""" + + async def coro_with_params(value, multiplier=2): + await asyncio.sleep(0.01) + return value * multiplier + + result = run_async_safely(coro_with_params(5, multiplier=3)) + assert result == 15 + + def test_run_sync_context_with_exception(self): + """Test run_async_safely properly propagates exceptions in sync context.""" + + async def failing_coro(): + await asyncio.sleep(0.01) + raise ValueError("Test error") + + with pytest.raises(ValueError, match="Test error"): + run_async_safely(failing_coro()) + + @pytest.mark.asyncio + async def test_run_async_context_simple(self): + """Test run_async_safely from within async context (uses threading).""" + + async def simple_coro(): + await asyncio.sleep(0.01) + return "from_thread" + + # This should use threading since we're already in an async context + result = run_async_safely(simple_coro()) + assert result == "from_thread" + + @pytest.mark.asyncio + async def test_run_async_context_with_exception(self): + """Test run_async_safely properly propagates exceptions from thread.""" + + async def failing_coro(): + await asyncio.sleep(0.01) + raise RuntimeError("Thread error") + + with pytest.raises(RuntimeError, match="Thread error"): + run_async_safely(failing_coro()) + + @pytest.mark.asyncio + async def test_run_async_context_thread_isolation(self): + """Test that threaded execution is properly isolated.""" + # Set a thread-local value in the main async context + threading.current_thread().test_value = "main_thread" + + async def check_thread_isolation(): + # This should run in a different thread + current_thread = threading.current_thread() + # Should not have the test_value from main thread + assert not hasattr(current_thread, "test_value") + return "isolated" + + result = run_async_safely(check_thread_isolation()) + assert result == "isolated" + + @pytest.mark.asyncio + async def test_run_async_context_preserves_contextvars(self): + """Test that threaded execution preserves the caller's contextvars.""" + request_id = contextvars.ContextVar("request_id") + token = request_id.set("req-123") + + async def read_contextvar(): + await asyncio.sleep(0.001) + return request_id.get() + + try: + result = run_async_safely(read_contextvar()) + assert result == "req-123" + finally: + request_id.reset(token) + + def test_multiple_calls_sync_context(self): + """Test multiple sequential calls in sync context.""" + + async def counter_coro(count): + await asyncio.sleep(0.001) + return count * 2 + + results = [] + for i in range(5): + result = run_async_safely(counter_coro(i)) + results.append(result) + + assert results == [0, 2, 4, 6, 8] + + @pytest.mark.asyncio + async def test_multiple_calls_async_context(self): + """Test multiple sequential calls in async context (each uses threading).""" + + async def counter_coro(count): + await asyncio.sleep(0.001) + return count * 3 + + results = [] + for i in range(3): + result = run_async_safely(counter_coro(i)) + results.append(result) + + assert results == [0, 3, 6] + + def test_concurrent_calls_sync_context(self): + """Test concurrent calls in sync context using threading.""" + + async def slow_coro(value): + await asyncio.sleep(0.02) + return value**2 + + import concurrent.futures + + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [] + for i in range(3): + future = executor.submit(run_async_safely, slow_coro(i + 1)) + futures.append(future) + + results = [future.result() for future in futures] + + # Results should be squares: 1^2, 2^2, 3^2 + assert sorted(results) == [1, 4, 9] + + def test_event_loop_detection_mock(self): + """Test event loop detection logic with mocking.""" + + async def simple_coro(): + return "mocked" + + # Mock no running loop - should use asyncio.run + with mock.patch( + "asyncio.get_running_loop", side_effect=RuntimeError("No loop") + ): + with mock.patch( + "asyncio.run", return_value="asyncio_run_called" + ) as mock_run: + result = run_async_safely(simple_coro()) + assert result == "asyncio_run_called" + mock_run.assert_called_once() + + def test_complex_coroutine(self): + """Test with a more complex coroutine that does actual async work.""" + + async def complex_coro(): + # Simulate some async operations + results = [] + for i in range(3): + await asyncio.sleep(0.001) + results.append(i**2) + + # Simulate concurrent operations + async def sub_task(x): + await asyncio.sleep(0.001) + return x * 10 + + tasks = [sub_task(x) for x in range(2)] + concurrent_results = await asyncio.gather(*tasks) + results.extend(concurrent_results) + + return results + + result = run_async_safely(complex_coro()) + assert result == [0, 1, 4, 0, 10] # [0^2, 1^2, 2^2, 0*10, 1*10] + + @pytest.mark.asyncio + async def test_nested_async_calls(self): + """Test that nested calls to run_async_safely work correctly.""" + + async def inner_coro(value): + await asyncio.sleep(0.001) + return value * 2 + + async def outer_coro(value): + # This is already in an async context, so the inner call + # will also use threading + inner_result = run_async_safely(inner_coro(value)) + await asyncio.sleep(0.001) + return inner_result + 1 + + result = run_async_safely(outer_coro(5)) + assert result == 11 # (5 * 2) + 1 + + def test_exception_types_preserved(self): + """Test that different exception types are properly preserved.""" + + async def custom_exception_coro(): + await asyncio.sleep(0.001) + + class CustomError(Exception): + pass + + raise CustomError("Custom error message") + + with pytest.raises(Exception) as exc_info: + run_async_safely(custom_exception_coro()) + + # The exception type should be preserved + assert "Custom error message" in str(exc_info.value) + + def test_return_types_preserved(self): + """Test that various return types are properly preserved.""" + + async def dict_coro(): + await asyncio.sleep(0.001) + return {"key": "value", "number": 42} + + async def list_coro(): + await asyncio.sleep(0.001) + return [1, 2, 3, "string"] + + async def none_coro(): + await asyncio.sleep(0.001) + return None + + dict_result = run_async_safely(dict_coro()) + assert dict_result == {"key": "value", "number": 42} + assert isinstance(dict_result, dict) + + list_result = run_async_safely(list_coro()) + assert list_result == [1, 2, 3, "string"] + assert isinstance(list_result, list) + + none_result = run_async_safely(none_coro()) + assert none_result is None + + @pytest.mark.asyncio + async def test_real_world_scenario_jupyter_simulation(self): + """Test scenario simulating Jupyter notebook environment.""" + # This simulates being called from a Jupyter cell where there's + # already an event loop running + + async def simulate_llm_call(prompt): + """Simulate an LLM API call.""" + await asyncio.sleep(0.01) # Simulate network delay + return f"Response to: {prompt}" + + async def simulate_experiment_task(item): + """Simulate an experiment task function.""" + response = await simulate_llm_call(item["input"]) + await asyncio.sleep(0.001) # Additional processing + return response + + # This should work even though we're in an async context + result = run_async_safely(simulate_experiment_task({"input": "test prompt"})) + assert result == "Response to: test prompt" diff --git a/tests/unit/test_version.py b/tests/unit/test_version.py new file mode 100644 index 000000000..883c7ee91 --- /dev/null +++ b/tests/unit/test_version.py @@ -0,0 +1,7 @@ +from importlib.metadata import version + +import langfuse + + +def test_package_version_matches_distribution_metadata(): + assert langfuse.__version__ == version("langfuse") diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 6b6849a6a..000000000 --- a/tests/utils.py +++ /dev/null @@ -1,116 +0,0 @@ -import base64 -import os -import typing -from time import sleep -from uuid import uuid4 - -try: - import pydantic.v1 as pydantic # type: ignore -except ImportError: - import pydantic # type: ignore - -from llama_index.core import ( - Settings, - SimpleDirectoryReader, - StorageContext, - VectorStoreIndex, - load_index_from_storage, -) -from llama_index.core.callbacks import CallbackManager - -from langfuse.api.client import FernLangfuse - - -def create_uuid(): - return str(uuid4()) - - -def get_api(): - sleep(2) - - return FernLangfuse( - username=os.environ.get("LANGFUSE_PUBLIC_KEY"), - password=os.environ.get("LANGFUSE_SECRET_KEY"), - base_url=os.environ.get("LANGFUSE_HOST"), - ) - - -class LlmUsageWithCost(pydantic.BaseModel): - prompt_tokens: typing.Optional[int] = pydantic.Field( - alias="promptTokens", default=None - ) - completion_tokens: typing.Optional[int] = pydantic.Field( - alias="completionTokens", default=None - ) - total_tokens: typing.Optional[int] = pydantic.Field( - alias="totalTokens", default=None - ) - input_cost: typing.Optional[float] = pydantic.Field(alias="inputCost", default=None) - output_cost: typing.Optional[float] = pydantic.Field( - alias="outputCost", default=None - ) - total_cost: typing.Optional[float] = pydantic.Field(alias="totalCost", default=None) - - -class CompletionUsage(pydantic.BaseModel): - completion_tokens: int - """Number of tokens in the generated completion.""" - - prompt_tokens: int - """Number of tokens in the prompt.""" - - total_tokens: int - """Total number of tokens used in the request (prompt + completion).""" - - -class LlmUsage(pydantic.BaseModel): - prompt_tokens: typing.Optional[int] = pydantic.Field( - alias="promptTokens", default=None - ) - completion_tokens: typing.Optional[int] = pydantic.Field( - alias="completionTokens", default=None - ) - total_tokens: typing.Optional[int] = pydantic.Field( - alias="totalTokens", default=None - ) - - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: - kwargs_with_defaults: typing.Any = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - return super().dict(**kwargs_with_defaults) - - -def get_llama_index_index(callback, force_rebuild: bool = False): - if callback: - Settings.callback_manager = CallbackManager([callback]) - PERSIST_DIR = "tests/mocks/llama-index-storage" - - if not os.path.exists(PERSIST_DIR) or force_rebuild: - print("Building RAG index...") - documents = SimpleDirectoryReader( - "static", ["static/state_of_the_union_short.txt"] - ).load_data() - index = VectorStoreIndex.from_documents(documents) - index.storage_context.persist(persist_dir=PERSIST_DIR) - else: - print("Using pre-built index from storage...") - storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR) - index = load_index_from_storage(storage_context) - - return index - - -def encode_file_to_base64(image_path) -> str: - with open(image_path, "rb") as file: - return base64.b64encode(file.read()).decode("utf-8") diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..7c321118f --- /dev/null +++ b/uv.lock @@ -0,0 +1,2456 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <4.0" + +[options] +exclude-newer = "2026-06-04T15:30:59.411452714Z" +exclude-newer-span = "P7D" + +[[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.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "autoevals" +version = "0.0.130" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chevron" }, + { name = "jsonschema" }, + { name = "polyleven" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/92/d80c8a7a34a2b64927ba0844fc6b71cf0c7224c4244d87f618cd3043da06/autoevals-0.0.130.tar.gz", hash = "sha256:92f87ab95a575b56d9d7377e6f1399932d09180d2f3a8266b4f693f46f49b86d", size = 51839, upload-time = "2025-09-08T05:30:01.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a0/f59dd73e8582c59672cf1f4e5f3ec60d1ee312f8f2a56ae54af5293173c7/autoevals-0.0.130-py3-none-any.whl", hash = "sha256:ffb7b3a21070d2a4e593bb118180c04e43531e608bffd854624377bd857ceec0", size = 56034, upload-time = "2025-09-08T05:29:59.908Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, + { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, + { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "chevron" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/1f/ca74b65b19798895d63a6e92874162f44233467c9e7c1ed8afd19016ebe9/chevron-0.14.0.tar.gz", hash = "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf", size = 11440, upload-time = "2021-01-02T22:47:59.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/93/342cc62a70ab727e093ed98e02a725d85b746345f05d2b5e5034649f4ec8/chevron-0.14.0-py3-none-any.whl", hash = "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443", size = 11595, upload-time = "2021-01-02T22:47:57.847Z" }, +] + +[[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 = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[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 = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.73.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, +] + +[[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 = "identify" +version = "2.6.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/bf/9ecc036fbc15cf4153ea6ed4dbeed31ef043f762cccc9d44a534be8319b0/jsonpointer-3.1.0.tar.gz", hash = "sha256:f9b39abd59ba8c1de8a4ff16141605d2a8dacc4dd6cf399672cf237bfe47c211", size = 9000, upload-time = "2026-03-20T21:47:09.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/25/cebb241a435cbf4626b5ea096d8385c04416d7ca3082a15299b746e248fa/jsonpointer-3.1.0-py3-none-any.whl", hash = "sha256:f82aa0f745001f169b96473348370b43c3f581446889c41c807bab1db11c8e7b", size = 7651, upload-time = "2026-03-20T21:47:08.792Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.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/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "langchain" +version = "1.2.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/e5/56fdeedaa0ef1be3c53721d382d9e21c63930179567361610ea6102c04ea/langchain-1.2.13.tar.gz", hash = "sha256:d566ef67c8287e7f2e2df3c99bf3953a6beefd2a75a97fe56ecce905e21f3ef4", size = 573819, upload-time = "2026-03-19T17:16:07.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1d/a509af07535d8f4621d77f3ba5ec846ee6d52c59d2239e1385ec3b29bf92/langchain-1.2.13-py3-none-any.whl", hash = "sha256:37d4526ac4b0cdd3d7706a6366124c30dc0771bf5340865b37cdc99d5e5ad9b1", size = 112488, upload-time = "2026-03-19T17:16:06.134Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.2.22" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/a3/c4cd6827a1df46c821e7214b7f7b7a28b189e6c9b84ef15c6d629c5e3179/langchain_core-1.2.22.tar.gz", hash = "sha256:8d8f726d03d3652d403da915126626bb6250747e8ba406537d849e68b9f5d058", size = 842487, upload-time = "2026-03-24T18:48:44.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/a6/2ffacf0f1a3788f250e75d0b52a24896c413be11be3a6d42bcdf46fbea48/langchain_core-1.2.22-py3-none-any.whl", hash = "sha256:7e30d586b75918e828833b9ec1efc25465723566845dd652c277baf751e9c04b", size = 506829, upload-time = "2026-03-24T18:48:43.286Z" }, +] + +[[package]] +name = "langchain-openai" +version = "0.3.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/23/91461c6c2b1c8ceeadd6fefe453a03b1537afeff4ace3c118d7e7659f5a7/langchain_openai-0.3.34.tar.gz", hash = "sha256:57916d462be5b8fd19e5cb2f00d4e5cf0465266a292d583de2fc693a55ba190e", size = 785924, upload-time = "2025-10-01T15:18:40.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/94/132aff5a1ed4ebfe7b4d3e4dbf30b9f70ce2bd2ac3470d0c0e742845afb1/langchain_openai-0.3.34-py3-none-any.whl", hash = "sha256:08d61d68a6d869c70d542171e149b9065668dedfc4fafcd4de8aeb5b933030a9", size = 75605, upload-time = "2025-10-01T15:18:39.415Z" }, +] + +[[package]] +name = "langfuse" +version = "4.8.0b1" +source = { editable = "." } +dependencies = [ + { name = "backoff" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "wrapt" }, +] + +[package.dev-dependencies] +dev = [ + { name = "autoevals" }, + { name = "langchain" }, + { name = "langchain-openai" }, + { name = "langgraph" }, + { name = "mypy" }, + { name = "openai" }, + { name = "opentelemetry-instrumentation-threading" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-httpserver" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "tenacity" }, +] +docs = [ + { name = "pdoc" }, +] + +[package.metadata] +requires-dist = [ + { name = "backoff", specifier = ">=1.10.0" }, + { name = "httpx", specifier = ">=0.15.4,<1.0" }, + { name = "opentelemetry-api", specifier = ">=1.33.1,<2" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.33.1,<2" }, + { name = "opentelemetry-sdk", specifier = ">=1.33.1,<2" }, + { name = "packaging", specifier = ">=23.2,<27.0" }, + { name = "pydantic", specifier = ">=2,<3" }, + { name = "wrapt", specifier = ">=1.14,<2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "autoevals", specifier = ">=0.0.130,<0.1" }, + { name = "langchain", specifier = ">=1,<2" }, + { name = "langchain-openai", specifier = ">=0.0.5,<0.4" }, + { name = "langgraph", specifier = ">=1,<2" }, + { name = "mypy", specifier = ">=1.0.0,<2" }, + { name = "openai", specifier = ">=0.27.8" }, + { name = "opentelemetry-instrumentation-threading", specifier = ">=0.59b0,<1" }, + { name = "pre-commit", specifier = ">=3.2.2,<4" }, + { name = "pytest", specifier = ">=7.4,<9.0" }, + { name = "pytest-asyncio", specifier = ">=0.21.1,<1.2.0" }, + { name = "pytest-httpserver", specifier = ">=1.0.8,<2" }, + { name = "pytest-timeout", specifier = ">=2.1.0,<3" }, + { name = "pytest-xdist", specifier = ">=3.3.1,<4" }, + { name = "ruff", specifier = ">=0.15.2,<0.16" }, + { name = "tenacity", specifier = ">=9.1.4" }, +] +docs = [{ name = "pdoc", specifier = ">=15.0.4,<16" }] + +[[package]] +name = "langgraph" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/b2/e7db624e8b0ee063ecfbf7acc09467c0836a05914a78e819dfb3744a0fac/langgraph-1.1.3.tar.gz", hash = "sha256:ee496c297a9c93b38d8560be15cbb918110f49077d83abd14976cb13ac3b3370", size = 545120, upload-time = "2026-03-18T23:42:58.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/f7/221cc479e95e03e260496616e5ce6fb50c1ea01472e3a5bc481a9b8a2f83/langgraph-1.1.3-py3-none-any.whl", hash = "sha256:57cd6964ebab41cbd211f222293a2352404e55f8b2312cecde05e8753739b546", size = 168149, upload-time = "2026-03-18T23:42:56.967Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/44/a8df45d1e8b4637e29789fa8bae1db022c953cc7ac80093cfc52e923547e/langgraph_checkpoint-4.0.1.tar.gz", hash = "sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9", size = 158135, upload-time = "2026-02-27T21:06:16.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/4c/09a4a0c42f5d2fc38d6c4d67884788eff7fd2cfdf367fdf7033de908b4c0/langgraph_checkpoint-4.0.1-py3-none-any.whl", hash = "sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034", size = 50453, upload-time = "2026-02-27T21:06:14.293Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/06/dd61a5c2dce009d1b03b1d56f2a85b3127659fdddf5b3be5d8f1d60820fb/langgraph_prebuilt-1.0.8.tar.gz", hash = "sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69", size = 164442, upload-time = "2026-02-19T18:14:39.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/41/ec966424ad3f2ed3996d24079d3342c8cd6c0bd0653c12b2a917a685ec6c/langgraph_prebuilt-1.0.8-py3-none-any.whl", hash = "sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0", size = 35648, upload-time = "2026-02-19T18:14:37.611Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.3.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/a1/012f0e0f5c9fd26f92bdc9d244756ad673c428230156ef668e6ec7c18cee/langgraph_sdk-0.3.12.tar.gz", hash = "sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71", size = 194932, upload-time = "2026-03-18T22:15:54.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/4d/4f796e86b03878ab20d9b30aaed1ad459eda71a5c5b67f7cfe712f3548f2/langgraph_sdk-0.3.12-py3-none-any.whl", hash = "sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327", size = 95834, upload-time = "2026-03-18T22:15:53.545Z" }, +] + +[[package]] +name = "langsmith" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/64/95f1f013531395f4e8ed73caeee780f65c7c58fe028cb543f8937b45611b/langsmith-0.8.0.tar.gz", hash = "sha256:59fe5b2a56bbbe14a08aa76691f84b49e8675dd21e11b57d80c6db8c08bac2e3", size = 4432996, upload-time = "2026-04-30T22:13:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/e1/a4be2e696c9473bb53298df398237da5674704d781d4b748ed35aeef592a/langsmith-0.8.0-py3-none-any.whl", hash = "sha256:12cc4bc5622b835a6d841964d6034df3617bdb912dae0c1381fd0a68a9b3a3ef", size = 393268, upload-time = "2026-04-30T22:13:05.56Z" }, +] + +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, + { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, + { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, + { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "openai" +version = "2.29.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/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/8f/8dedba66100cda58af057926449a5e58e6c008bec02bc2746c03c3d85dcd/opentelemetry_instrumentation_threading-0.61b0.tar.gz", hash = "sha256:38e0263c692d15a7a458b3fa0286d29290448fa4ac4c63045edac438c6113433", size = 9163, upload-time = "2026-03-04T14:20:50.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/77/c06d960aede1a014812aa4fafde0ae546d790f46416fbeafa2b32095aae3/opentelemetry_instrumentation_threading-0.61b0-py3-none-any.whl", hash = "sha256:735f4a1dc964202fc8aff475efc12bb64e6566f22dff52d5cb5de864b3fe1a70", size = 9337, upload-time = "2026-03-04T14:19:57.983Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, + { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657", size = 378262, upload-time = "2026-01-18T20:55:22.284Z" }, + { url = "https://files.pythonhosted.org/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163", size = 203034, upload-time = "2026-01-18T20:55:53.324Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a", size = 210538, upload-time = "2026-01-18T20:55:20.097Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2", size = 212401, upload-time = "2026-01-18T20:55:46.447Z" }, + { url = "https://files.pythonhosted.org/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd", size = 387080, upload-time = "2026-01-18T20:56:00.884Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c", size = 482346, upload-time = "2026-01-18T20:55:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b", size = 425178, upload-time = "2026-01-18T20:55:27.106Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f", size = 117183, upload-time = "2026-01-18T20:55:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pdoc" +version = "15.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/5c/e94c1ab4aa2f8a9cc29d81e1c513c6216946cb3a90957ef7115b12e9363d/pdoc-15.0.4.tar.gz", hash = "sha256:cf9680f10f5b4863381f44ef084b1903f8f356acb0d4cc6b64576ba9fb712c82", size = 155678, upload-time = "2025-06-04T17:05:49.639Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/2c/87250ac73ca8730b2c4e0185b573585f0b42e09562132e6c29d00b3a9bb9/pdoc-15.0.4-py3-none-any.whl", hash = "sha256:f9028e85e7bb8475b054e69bde1f6d26fc4693d25d9fa1b1ce9009bec7f7a5c4", size = 145978, upload-time = "2025-06-04T17:05:48.473Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[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 = "polyleven" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/c7/e0b3bbe72e0003e5d02726e0d406ea47d523a2aec9c41d831817a8e0bce1/polyleven-0.11.0.tar.gz", hash = "sha256:d74d348387cf340051711c0dd6af993b4c264daa78470098de16f4a2b725785c", size = 6407, upload-time = "2026-02-09T09:41:49.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/6c/34c6189c80adf7575fb2daee38c4b836c154e416ab3c14d17afa7f88b9c3/polyleven-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ccf87f6ac8d76aa4c48a4828becc0c19fd1589b14b20affe23e5e012be4fa64f", size = 7421, upload-time = "2026-02-09T09:40:33.243Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/b20d3c9f9b6bded43a0388037044f2bcb1add20fa9a758d1144d79e09d10/polyleven-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1a02e3f0acfd1164cbaea25192398bc943ee9b93b9883a1fba9b2613d3616b0", size = 7514, upload-time = "2026-02-09T09:40:34.514Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d8/60290fd8d8298671edb4ce221d8ee4d81156b3c21b1154a615da7ea8e57d/polyleven-0.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6526d2516b439065864722069de6fcc418a4135696990dad66b81ddb18863bd9", size = 19556, upload-time = "2026-02-09T09:40:35.868Z" }, + { url = "https://files.pythonhosted.org/packages/c1/cd/17f1f6009a344c18f4213edcc97a9e7dae22e9a26e722aae10052378a43e/polyleven-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65fc01fe6cfe287f2f20170b35687a436ab36b882db568a55d81d6e0acd8379d", size = 20303, upload-time = "2026-02-09T09:40:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/4e/18/30c7da8056adc4b9a81775f5b1810a2c9b6cc87fc34c8cf4c0280d28cab6/polyleven-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4b8e5ac8faecc6daa7b3d325436a3f23f8c33dec7bfca5d22df3fbe00f92ddd9", size = 19565, upload-time = "2026-02-09T09:40:38.471Z" }, + { url = "https://files.pythonhosted.org/packages/de/14/86e9c33ff9fda84297556373a6376100cbe9bb5d917fc3421dce4ada441b/polyleven-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc4f17007b07fde292ad33ec43a3ae8febe27a5bd92462b920736fd81d774fce", size = 19365, upload-time = "2026-02-09T09:40:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/be/18/1341f7860bbe2287f6cb8a540c3435c504cfc58659b67106ab60f695175e/polyleven-0.11.0-cp310-cp310-win32.whl", hash = "sha256:cae70197d545a09bfab8d7e506eed66ef314fa6c4e7a5e2c402c2febc31db74b", size = 11672, upload-time = "2026-02-09T09:40:40.822Z" }, + { url = "https://files.pythonhosted.org/packages/6d/09/ed2cb3dbec7a925d80107516b06dfe10dd368c6abfb43765df6feb6cd551/polyleven-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:bf47079b6dc62e6af2bd6ecb45a6087efd9a27b61666b98d0326c246a22ea991", size = 10828, upload-time = "2026-02-09T09:40:42.224Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/cb74c2ffe4e35935d80ef6f180f9e0987b8000917d52050a3563b32e1e73/polyleven-0.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:4c78b4d3e7d7b74315d5422178118963374c0cf3d7a9532a955f446ed365320a", size = 9389, upload-time = "2026-02-09T09:40:43.172Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7a/27ea9a78b617ddb14c2f5d2416df2fbf07fa5e52685f2968686a0308c8af/polyleven-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a28860fe33a7f907bc5f86e55a0b9faea80047d1677fa23b4d6c631ccf91ef2f", size = 7420, upload-time = "2026-02-09T09:40:44.505Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/fea309d41502aa5a344a6d4d6e5b8bdabb1df1e28f1af52bb53180f6c956/polyleven-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:47a3fb5b8cb60f647d2832d38b7d87cda27da8622b27c1292bceb9a04954c189", size = 7514, upload-time = "2026-02-09T09:40:46.44Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/c7d3bb6c66050304c3fe3cae1a716f62fea947ac3f14d02ef71e24422f76/polyleven-0.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:209fa669ca23ac453a7e9fbf07695350d5cbe61d71a6226b861757ccab28e664", size = 20887, upload-time = "2026-02-09T09:40:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/aeaf38075c7e0225fd7ba89db3bd11c0e65b50907242d82d57c4804941d9/polyleven-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3bfce4689b6aaacf7c5296b8ed11ada07ccf046a01097ba1681e10f9caabbf6f", size = 21376, upload-time = "2026-02-09T09:40:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/bd/96/10f01f8ab883a51ef7bed610933ba88b7cf5b0a0e3fd059c94f1db8414d4/polyleven-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:83e59c8590a06ea6a959a3c55e6d28544b8d11a51aac2a318c1b74f92575dd28", size = 20436, upload-time = "2026-02-09T09:40:50.059Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4e/5cedf4cfde32eddab388435463a0f8c2e322449c61ef4765db62ca7c0d13/polyleven-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2eb8f6778f3073dce041805a09f0753cc441b0219253d7b933aad234a954f30a", size = 20685, upload-time = "2026-02-09T09:40:51.03Z" }, + { url = "https://files.pythonhosted.org/packages/a0/40/d88e50b60d0a731d9fb7e71268a91fcf8e290eaa5c043715d8f6ad158fe2/polyleven-0.11.0-cp311-cp311-win32.whl", hash = "sha256:248b9f645d8c6e337091498ed5c7d4a796d9d51df98458be25b1d76d962954e2", size = 11613, upload-time = "2026-02-09T09:40:52.468Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/a52aeeb5200ea4c6d1642cd9e86827feee619e56792d1795465532a9d6f8/polyleven-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:5c9ccb2f327d49a7566b0192e0d426f7772b38e247dc4e809c0b1cdf23e2ecc2", size = 10814, upload-time = "2026-02-09T09:40:53.355Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e1/857fa37a1d4cca74cb2a144cd2962d265fd2d705f971c146d6ee6cdab546/polyleven-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:9b97b9260730deda4cdd5878fd6ce128b970497da0fbefeeacc0b1ed4c59ebb7", size = 9420, upload-time = "2026-02-09T09:40:54.59Z" }, + { url = "https://files.pythonhosted.org/packages/9b/31/a9b7aa80589d54bde7d894a9fb118a766833def3ded11739e70b1f19d951/polyleven-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a33728df4708c9370f5e65fdb8de7e15f01d5ae8530eedf507d182fe63afb0e", size = 7437, upload-time = "2026-02-09T09:40:55.459Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5e/71ee3cb252fd6abdf19dd40c11f87ad3adffa06cc0b8098b98ec355573a4/polyleven-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58c11ce44466f6d833fd90f77ccd0c44accce41c9e80dea4c2817d5c124a61d5", size = 7510, upload-time = "2026-02-09T09:40:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1b/4c41947cfb2f7c4348d60d53d45418e47c305e33b1ce78218b84d636fca8/polyleven-0.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ce2e782f8fae812c7ad960c4fd17a58ada183b89ae220cacfe6b3234179872f4", size = 20982, upload-time = "2026-02-09T09:40:57.152Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/07e4f90873e4c0c764f6081ad44c17308d7d18976284d80bca47c98e4282/polyleven-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a472fccba89ffb44b10481760c7351c79855b0ff654ec9d28966bfb111a71748", size = 21476, upload-time = "2026-02-09T09:41:00.3Z" }, + { url = "https://files.pythonhosted.org/packages/67/26/31871030852e62e705d060c1dafaddd2f9a4b3664a8a6866ad7521e7e07f/polyleven-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:758c5fcb9d8556720fb51c1de5f3a7b39bb8dce9510a1f40e5f287951b901010", size = 20497, upload-time = "2026-02-09T09:41:01.252Z" }, + { url = "https://files.pythonhosted.org/packages/71/8a/8e1b60f5fc905c0437a063414eee58fcc775b8ae6229e85439b6c477b331/polyleven-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:367faf0e1898c79624f46a894ebe5b69bf1782318ec3e3331676ce5b24352882", size = 20768, upload-time = "2026-02-09T09:41:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/7f9bab45279828b9d70a0674d92fc78e858dcbfa79b1e0669f93b039249e/polyleven-0.11.0-cp312-cp312-win32.whl", hash = "sha256:71bbb17919548d4e162444c918b1acf864f84150197087c757975606cbb99e43", size = 11625, upload-time = "2026-02-09T09:41:03.189Z" }, + { url = "https://files.pythonhosted.org/packages/e5/35/47a019878909f80526567347379ba73aff114c5ee6d6c6efb7a0b6d4573b/polyleven-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:e7b6c8cfa13114bc2b17b51503a4db0cbc358c3c96197d6d7283bd686c0fd8fb", size = 10834, upload-time = "2026-02-09T09:41:04.065Z" }, + { url = "https://files.pythonhosted.org/packages/3e/40/4e80a66231052693328fc866a932e07b636cf8bb7ddf0eb54aa475f792bf/polyleven-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:561f028c9535223c78cd58f6b546dd15ce69a6e268e651ae1377644845fae639", size = 9424, upload-time = "2026-02-09T09:41:05.225Z" }, + { url = "https://files.pythonhosted.org/packages/ce/16/5aec69609adc373f10087eb69b0b9d177ae721632715a86348b429030514/polyleven-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cb8ed97b536f9aada3ad45169ee7768c426498bf3fa608a4eabd055dfef795e", size = 7425, upload-time = "2026-02-09T09:41:06.542Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5b/0542c723aa83833a5090114bc4e5a8e60293873fe60ee8221a5888d87370/polyleven-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f975ab8cb81fd8eb5a647a3cefb0bb80bc307920a9307f66ab4019d88370ed2", size = 7505, upload-time = "2026-02-09T09:41:07.445Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8d/c317217734a5bd2011f1128c1a9056477a5148d8d95527fcab2fe3955876/polyleven-0.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16986fd58911d6075b5f63ea001197141145b7a6df48bc4ce4530e79227e74a2", size = 21035, upload-time = "2026-02-09T09:41:08.32Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/8a3e6e4a68dbd9de564fd3d16eee90e3f807a4380fd7192f40af4be47175/polyleven-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6a814629cc0468f9800b1333414a3be08fda9c5ce6b63e97154a9d21732e590", size = 21509, upload-time = "2026-02-09T09:41:09.285Z" }, + { url = "https://files.pythonhosted.org/packages/6b/da/4097998bea845f0b3a67112200aa08c19d4da0a17d761b35484d695c21e2/polyleven-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88a35ec93ec3d81a7347fd49db314a914798a144dca3d22946d18bba9b597dec", size = 20536, upload-time = "2026-02-09T09:41:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/a1/71/67b7679ede99589ec749290d938693b87cdb6bb327b062c46d2129a5e6ec/polyleven-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:50bb7d68b790194d552ee1256a02e205486b27eb22ab333eeb0003e0271c4846", size = 20775, upload-time = "2026-02-09T09:41:11.692Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3e/6f7fad4fee748ba365cb3e1ba2e061a74e18d987eb554ead4757127df2ab/polyleven-0.11.0-cp313-cp313-win32.whl", hash = "sha256:ce264f6a9daa3265299d8ffcb180d8256517a8d9235613a3b267172da0bc1e06", size = 11629, upload-time = "2026-02-09T09:41:12.652Z" }, + { url = "https://files.pythonhosted.org/packages/2f/cc/4877913dec8fb4f968a070c894254db5811b62128d3a69b05bcd1305b5c3/polyleven-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4648732c8ad3955c8d7b1aa015d92936a150475aaa97ce704fe0c8e7fa7e0c4f", size = 10841, upload-time = "2026-02-09T09:41:13.682Z" }, + { url = "https://files.pythonhosted.org/packages/59/e2/039cc477ce73d6184e12cf6341ac200bc9f4c5428254c399015ec30392e1/polyleven-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:166f6c9b161c6af92ff201c734d6437bc7ef74a32dab306c5d47a0bdb7a82d9f", size = 9424, upload-time = "2026-02-09T09:41:14.545Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/a02d74f965127adb6a8fbd5030e2c98335ef2f8e7452b12a882883b2053a/polyleven-0.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3c18b8e44e5d04f1ffa7d41eb68da553833ab8663b7cfb1a505d85676db5c797", size = 7482, upload-time = "2026-02-09T09:41:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/fe/74/dfa9e9891cd85e679f230c5e740cba11b0bb11bd9fb298657ccf048ff70e/polyleven-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7ab547adc0ac72a2852d37337a4a839d4e2f713940b0e8a944d45c528e5e6538", size = 7508, upload-time = "2026-02-09T09:41:16.365Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ef/399ae8d21f7b348514b7ad3bd7b9d530bf195fb0a8ec63cf7af7d17a4071/polyleven-0.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5808f62874187dfd4e30de5dd5f42a660562ec95a87cc64d5455ba0f4be8f175", size = 21056, upload-time = "2026-02-09T09:41:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/21/60/7eb97286a6171dd794a0e5b261175e8bfeb99a2b566bd9b8848ebc97f6df/polyleven-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9deb75346b4177d5e69496791e6156f705d9059961ce8f9520a0dc96532f10f2", size = 21535, upload-time = "2026-02-09T09:41:18.137Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/6fa59257c2138e33a858f10236a2a6b381b87f61251c1df468be7c666338/polyleven-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ef28c4c6cdc71a32f0478772d2f07b2cd412fe7950182033b1c36c8a481b0834", size = 20560, upload-time = "2026-02-09T09:41:19.04Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2d/85be9c91d05cb0127586640108f3110f6a3a98c9478f84713d4771c49761/polyleven-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94832ff5d04022ba6038c2ca0c9ea6906330cde3a3b1761739d772647d01da33", size = 20814, upload-time = "2026-02-09T09:41:20.001Z" }, + { url = "https://files.pythonhosted.org/packages/da/91/5a99ae6cf16ff55a94c5686871ed20b816ad1690f823494c76dc3ce0f54b/polyleven-0.11.0-cp314-cp314-win32.whl", hash = "sha256:e6182ea6142904ea50cf82e2955d922156b5fcf9a8279925f312961f16710a58", size = 11966, upload-time = "2026-02-09T09:41:20.946Z" }, + { url = "https://files.pythonhosted.org/packages/48/ec/9c6fcdeb1dd436523f8e2275407f588d6a66a524d7a793f554957373769c/polyleven-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:bf82bb8601582da8f2248293c1e6f4cce2025c79fd64fccddf67dd8538655b55", size = 11100, upload-time = "2026-02-09T09:41:21.863Z" }, + { url = "https://files.pythonhosted.org/packages/42/7f/1e59881a56a4963b4546c7b558ab7979daddff586001f18b80f1f66cece9/polyleven-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:45487a1e4a8415e4ed45e6720b2a3ad9d240336f7afa136a625b8f802a1880c2", size = 9624, upload-time = "2026-02-09T09:41:22.749Z" }, + { url = "https://files.pythonhosted.org/packages/47/5a/5eaa75427f17d4cdf8e2139988a3ec6b841b6e077ebc1fccb754c1f8b55e/polyleven-0.11.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c518ced3e7c05de4efbd12fd7b61d6d574eb170f431e0415689d9f143fe552ee", size = 7490, upload-time = "2026-02-09T09:41:23.677Z" }, + { url = "https://files.pythonhosted.org/packages/50/47/5dd5fa13d315e0d5dc3e41bbaa16306ea56e74929ad29df54d5c24a84dcc/polyleven-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa49732cdecd985241db9f78d5fdba7170ba6375d2bf9ad040b05127dc96b877", size = 7514, upload-time = "2026-02-09T09:41:24.55Z" }, + { url = "https://files.pythonhosted.org/packages/75/aa/838f1bc632144f4f5820b9dbd31e0c64de41a7b0970b5cbe6fc02746090f/polyleven-0.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2aada9dd04e84389d90790f359447447a499d6d86807697d80732ed45547a43", size = 21123, upload-time = "2026-02-09T09:41:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/d6f32263b863dfffeed9a67e80b53476cd0089f202b0510a80eb07f7425b/polyleven-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94311ee39e2db957415eacb36b96ae26dcc427c260465324de45fb8c870d4661", size = 21627, upload-time = "2026-02-09T09:41:27.219Z" }, + { url = "https://files.pythonhosted.org/packages/ae/68/4dee05a4217a3eb1f85cbc915f5fa269d79b86d2a8384be68bcd21de37cc/polyleven-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:45cfb234fece0c9df73276788fa529a25f91abf97dd0d9aed4f1b713b6d530e3", size = 20635, upload-time = "2026-02-09T09:41:28.137Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c2/8486bdaebf47e6b764e8be227a7d2898463f2b4d91443ecdeee9ebeca6bc/polyleven-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aaed455f498172769fd88f83c27bb8f43e0583d7b27d6b343154d471ec2145e", size = 20870, upload-time = "2026-02-09T09:41:29.07Z" }, + { url = "https://files.pythonhosted.org/packages/b3/13/b827188b55108bd816110a6f60b78aee0db045a98bf7b1f2e7bfb60f4039/polyleven-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2a59849c327279902e8b396666f6998234aa82aacc47abc103d93babaad46203", size = 11917, upload-time = "2026-02-09T09:41:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/ab/18/c909bde1d1db7ead33329b941b0050c93cab9b811e44b49d04adb8c5f0f8/polyleven-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ba2dcf3aff2909bbf3bdd9c1749f8de207f023fbb2c0b1d681c6bf3e78ceef1", size = 11073, upload-time = "2026-02-09T09:41:31.371Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/51f7a0fab2d65c2b6908872f26bb03bb7e2357d195f2a59aec1a27489106/polyleven-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:05207bb66da15a2dc5c530e2f5cb5f0588d0a7e79b3bd542965f9e06e3fb14fe", size = 9601, upload-time = "2026-02-09T09:41:32.235Z" }, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815, upload-time = "2024-07-28T19:59:01.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643, upload-time = "2024-07-28T19:58:59.335Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +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-asyncio" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/1e/2aa43805d4a320a9489d2b99f7877b69f9094c79aa0732159a1415dd6cd4/pytest_asyncio-1.1.1.tar.gz", hash = "sha256:b72d215c38e2c91dbb32f275e0b5be69602d7869910e109360e375129960a649", size = 46590, upload-time = "2025-09-12T06:36:20.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/aba79e9ccdb51b5d0d65c67dd857bd78b00c64723df16b9fc800d8b94ce6/pytest_asyncio-1.1.1-py3-none-any.whl", hash = "sha256:726339d30fcfde24691f589445b9b67d058b311ac632b1d704e97f20f1d878da", size = 14719, upload-time = "2025-09-12T06:36:19.726Z" }, +] + +[[package]] +name = "pytest-httpserver" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/17/ad187f46998814014f7cda309de700b87c0eb4b2e111e18bc8c819be7116/pytest_httpserver-1.1.5.tar.gz", hash = "sha256:dc3d82e1fe00e491829d8939c549bf4bd9b39a260f87113c619b9d517c2f8ff1", size = 70974, upload-time = "2026-02-14T13:27:23.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/df/0bdf90b84c6a586a9fd2b509523a3ab26b1cc1b1dba2fb62a32e4411ea9e/pytest_httpserver-1.1.5-py3-none-any.whl", hash = "sha256:ee83feb587ab652c0c6729598db2820e9048233bac8df756818b7845a1621d0a", size = 23330, upload-time = "2026-02-14T13:27:22.119Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/90/bcce6b46823c9bec1757c964dc37ed332579be512e17a30e9698095dcae4/python_discovery-1.2.0.tar.gz", hash = "sha256:7d33e350704818b09e3da2bd419d37e21e7c30db6e0977bb438916e06b41b5b1", size = 58055, upload-time = "2026-03-19T01:43:08.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/3c/2005227cb951df502412de2fa781f800663cccbef8d90ec6f1b371ac2c0d/python_discovery-1.2.0-py3-none-any.whl", hash = "sha256:1e108f1bbe2ed0ef089823d28805d5ad32be8e734b86a5f212bf89b71c266e4a", size = 31524, upload-time = "2026-03-19T01:43:07.045Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +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/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" }, + { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" }, + { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" }, + { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" }, + { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" }, + { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[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, upload-time = "2023-05-01T04:11:33.229Z" } +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, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, + { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, + { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, +] + +[[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 = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, + { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, + { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, + { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]